Version: 1.0.0 | Requires WordPress: 6.4+ | Requires PHP: 8.0+ | License: GPL-2.0-or-later
DSGVO-compliant WordPress restaurant theme by Maximilian Kohler. Built with three core goals: full DSGVO compliance (zero external requests, self-hosted fonts, OSM map with consent gate), no required plugins, and no dependency on the block editor. All front-end output is controlled through page templates and inc/ modules; the site admin fills in content through a dedicated settings page.
- PHP 8.0 or higher
- WordPress 6.4 or higher
- No plugins required
Copy the theme folder into your WordPress installation:
wp-content/themes/tisch-kohler/
The folder must be named exactly tisch-kohler.
Go to Appearance > Themes and activate "Tisch by Kohler".
Create the following pages in Pages > Add New. Each page must use the exact slug shown and have the corresponding page template selected under Page Attributes > Template.
| Slug | Template Name |
|---|---|
tagesessen |
Tagesessen |
speisekarte |
Speisekarte |
catering |
Catering |
reservierung |
Reservierung |
ueber-uns |
Über uns |
kontakt |
Kontakt |
impressum |
Impressum |
datenschutzerklaerung |
Datenschutzerklärung |
Create one additional page for the front page (e.g. titled "Startseite") — no template needed; it uses front-page.php automatically.
Go to Settings > Reading and set:
- "Your homepage displays" → A static page
- "Homepage" → the Startseite page you just created
Go to Appearance > Menus:
- Create a menu named "Hauptnavigation". Add the pages you want visible in the top navigation. Assign it to the Hauptnavigation location.
- Create a menu named "Footer-Navigation". It must include the Impressum and Datenschutzerklärung pages to satisfy DSGVO requirements. Assign it to the Footer-Navigation location.
Go to Appearance > Tisch Einstellungen and fill in all contact, hours, and content settings. See the Admin Settings Reference below.
All settings are under Appearance > Tisch Einstellungen. Save with the button at the bottom of the page.
| Field | Description |
|---|---|
| E-Mail-Adresse | Contact email; also receives reservation requests |
| Telefon | Phone number; displayed in footer and contact page |
| Adresse (Footer) | Single-line address shown in the site footer |
One row per day (Montag through Sonn- und Feiertag).
| Field | Description |
|---|---|
| Ruhetag / Geschlossen | Check to mark the day as closed; disables the hours textarea |
| Hours textarea | Free-text field for opening times (e.g. 11:30 – 14:00 Uhr) |
| Hinweis | Optional note shown below the hours table (accepts basic HTML) |
Three slots for planned closing periods. When today's date falls within a period, a notice banner is displayed on every page of the site.
| Field | Description |
|---|---|
| Von | Start date (date picker, stored as YYYY-MM-DD) |
| Bis | End date (date picker, stored as YYYY-MM-DD) |
| Bezeichnung | Label for the period, e.g. "Weihnachtsferien" |
Leave a slot's dates empty to disable it.
| Field | Description |
|---|---|
| PDF-Datei | Upload a PDF via the media library. The URL and attachment ID are stored automatically. |
| Gültig bis | Expiry date. When today's date is past this date, the PDF viewer is replaced by a notice and the restaurant phone number. Leave empty for no expiry. |
| Field | Description |
|---|---|
| PDF-Datei | Optional PDF download (same media-library picker as Tagesessen). |
| Gültig bis | When exceeded the download button is hidden. Leave empty for no expiry. |
A dynamic repeater for the structured menu displayed on the Speisekarte page. Sections can be added, reordered with the arrow buttons, and removed. Each section has a title and any number of dishes.
Per dish: Name (required), Preis, Beschreibung (optional), Allergene (optional).
| Field | Description |
|---|---|
| Tagline | Short text displayed in italic below the site name in the hero section |
| Willkommenstext | Longer welcome paragraph; supports basic formatting via the mini editor |
| Hero-Bild | Background image for the hero; use a landscape image of at least 1920 × 800 px |
| Field | Description |
|---|---|
| Breitengrad (Lat) | Decimal latitude, e.g. 48.087400 |
| Längengrad (Lng) | Decimal longitude, e.g. 9.218900 |
Color overrides are set in Appearance > Anpassen > Tisch by Kohler > Farben. Live preview is supported.
| Control | CSS variable overridden | Default |
|---|---|---|
| Primärfarbe (Braun) | --color-primary |
#5C3D2E |
| Primärfarbe dunkel | --color-primary-dark |
#3E2418 |
| Akzentfarbe (Gold) | --color-accent |
#C8922A |
| Hintergrundfarbe | --color-bg |
#FAF6F0 |
| Template Name | Slug | Purpose |
|---|---|---|
| Tagesessen | tagesessen |
Shows the weekly menu PDF with expiry logic |
| Speisekarte | speisekarte |
PDF download bar + structured menu repeater |
| Catering | catering |
Static catering information page |
| Reservierung | reservierung |
Online reservation form |
| Über uns | ueber-uns |
About page with free WordPress editor content |
| Kontakt | kontakt |
Contact details + OSM map with consent gate |
| Impressum | impressum |
Legal notice page |
| Datenschutzerklärung | datenschutzerklaerung |
Privacy policy page |
tisch-kohler/
├── style.css # Theme header; imports assets/css/main.css
├── functions.php # Autoloader only — requires inc/*.php in order
├── index.php # Fallback template (empty, required by WordPress)
├── front-page.php # Homepage: hero, welcome, tagesessen teaser, hours
├── header.php # Skip link, site header, sticky nav, closing banner
├── footer.php # Site footer, nav menus, copyright
├── page.php # Default page template (WP editor content)
├── 404.php # 404 error page
│
├── inc/
│ ├── helpers.php # Shared utility functions (see Helper Functions)
│ ├── theme-setup.php # after_setup_theme: supports, menus, image sizes
│ ├── enqueue.php # CSS/JS registration, conditional loading
│ ├── customizer.php # Customizer color controls (Appearance > Anpassen)
│ ├── security.php # Hardening hooks (see Security Hardening)
│ ├── options.php # Admin settings page (Appearance > Tisch Einstellungen)
│ └── reservation-form.php # Reservation POST handler
│
├── page-templates/
│ ├── tagesessen.php
│ ├── speisekarte.php
│ ├── catering.php
│ ├── reservierung.php
│ ├── ueber-uns.php
│ ├── kontakt.php
│ ├── impressum.php
│ └── datenschutz.php
│
├── template-parts/
│ ├── home/
│ │ ├── hero.php # Full-bleed hero with background image/gradient
│ │ ├── welcome.php # Welcome text section
│ │ ├── tagesessen-teaser.php # Teaser card linking to tagesessen page
│ │ └── opening-hours.php # Hours table driven by options
│ ├── tagesessen/
│ │ └── pdf-viewer.php # PDF embed with expiry logic
│ ├── reservierung/
│ │ └── reservation-form.php # The reservation HTML form
│ └── kontakt/
│ └── osm-map.php # OSM iframe with consent overlay
│
├── assets/
│ ├── css/
│ │ ├── main.css # All front-end styles (design tokens + components)
│ │ └── print.css # Print-only overrides
│ ├── fonts/
│ │ ├── fonts.css # @font-face declarations
│ │ └── *.woff2 # Playfair Display 700, Lato 400 & 700
│ └── js/
│ ├── navigation.js # Mobile menu toggle
│ ├── reservation.js # Client-side form validation
│ ├── osm-consent.js # OSM map consent gate (localStorage)
│ ├── customizer-preview.js # Live preview in Customizer
│ └── admin.js # Media uploader + Speisekarte repeater (admin only)
│
└── languages/
└── tisch-kohler.pot # Translation template
functions.php contains no logic; it requires the following inc/ files in this order:
helpers.phptheme-setup.phpenqueue.phpcustomizer.phpsecurity.phpoptions.phpreservation-form.php
All design tokens are declared in assets/css/main.css under :root.
Colors
| Variable | Value | Usage |
|---|---|---|
--color-primary |
#5C3D2E |
Walnut brown — headings, buttons, borders |
--color-primary-dark |
#3E2418 |
Hover states, footer background |
--color-primary-light |
#7A5545 |
Subtle highlights |
--color-accent |
#C8922A |
Amber gold — CTAs, icons, underlines |
--color-accent-dark |
#A07020 |
Accent hover |
--color-accent-light |
#E8B86D |
Accent on dark backgrounds |
--color-bg |
#FAF6F0 |
Warm cream — page background |
--color-surface |
#FFF9F2 |
Card/header background |
--color-surface-alt |
#F2EAE0 |
Alternate section background |
--color-text |
#2C1810 |
Dark brown — body text |
--color-text-muted |
#6B5B4E |
Secondary text |
--color-text-inverse |
#FAF6F0 |
Text on dark backgrounds |
--color-border |
#D4B896 |
Default border |
--color-border-light |
#E8DDD0 |
Subtle dividers |
--color-success |
#3A7D44 |
Form success state |
--color-success-bg |
#EBF5EC |
Success notice background |
--color-error |
#9B2335 |
Form error state |
--color-error-bg |
#FBEAEA |
Error notice background |
Typography
| Variable | Value |
|---|---|
--font-heading |
'Playfair Display', Georgia, 'Times New Roman', serif |
--font-body |
'Lato', system-ui, -apple-system, sans-serif |
--text-xs |
clamp(0.75rem, 1.5vw, 0.875rem) |
--text-sm |
clamp(0.875rem, 2vw, 1rem) |
--text-base |
clamp(1rem, 2.5vw, 1.125rem) |
--text-lg |
clamp(1.125rem, 2.5vw, 1.25rem) |
--text-xl |
clamp(1.25rem, 3vw, 1.5rem) |
--text-2xl |
clamp(1.5rem, 4vw, 2rem) |
--text-3xl |
clamp(1.875rem, 5vw, 2.5rem) |
--text-4xl |
clamp(2.25rem, 6vw, 3.5rem) |
Spacing
--space-1 (0.25rem) through --space-24 (6rem) in common increments.
Shadows
| Variable | Value |
|---|---|
--shadow-sm |
0 1px 3px rgba(44,24,16,0.08) |
--shadow-md |
0 4px 16px rgba(44,24,16,0.12) |
--shadow-lg |
0 8px 32px rgba(44,24,16,0.16) |
Layout
| Variable | Value |
|---|---|
--container-max |
1200px |
--container-narrow |
760px |
--container-padding |
clamp(1rem, 5vw, 2rem) |
--header-height |
72px |
Transitions
| Variable | Value |
|---|---|
--transition-fast |
150ms ease |
--transition-normal |
250ms ease |
| File | Purpose | Dependencies | Loading |
|---|---|---|---|
navigation.js |
Mobile hamburger menu toggle; keyboard-accessible (Escape key, outside-click close) | None | defer, all pages |
reservation.js |
Client-side validation of the reservation form; highlights empty required fields | None | defer, Reservierung template only |
osm-consent.js |
Shows a consent overlay before loading the OSM iframe; stores consent in localStorage |
None | defer, Kontakt template only |
customizer-preview.js |
Applies Customizer color changes as live CSS variable updates in the preview frame | customize-preview |
Customizer only |
admin.js |
WordPress media uploader integration for PDF/image pickers; Speisekarte section/item repeater | jQuery | Admin settings page only |
Two menu locations are registered:
| Location slug | Label | Fallback |
|---|---|---|
primary |
Hauptnavigation | tisch_nav_fallback() — renders a single "Startseite" link |
footer |
Footer-Navigation | tisch_footer_nav_fallback() — links to Impressum and Datenschutzerklärung pages if they exist |
| Size name | Dimensions | Crop |
|---|---|---|
tisch-hero |
1920 × 800 | Hard crop |
tisch-card |
600 × 400 | Hard crop |
tisch-thumb |
300 × 200 | Hard crop |
Self-hosted .woff2 files in assets/fonts/. No requests to Google Fonts or any external CDN.
| Font | Weights |
|---|---|
| Playfair Display | 700 |
| Lato | 400, 700 |
@font-face declarations live in assets/fonts/fonts.css, enqueued as tisch-fonts before the main stylesheet.
All values are retrieved with get_option( $key ).
| Key | Sanitizer | Default |
|---|---|---|
tisch_email |
sanitize_email |
'' |
tisch_phone |
sanitize_text_field |
'' |
tisch_address |
sanitize_text_field |
'' |
Repeat for each day key: mon, tue, wed, thu, fri, sat, sun.
| Key pattern | Sanitizer | Default |
|---|---|---|
tisch_hours_{day} |
sanitize_textarea_field |
'' |
tisch_hours_{day}_closed |
sanitize_text_field |
'' (empty = open, '1' = closed) |
tisch_hours_note |
wp_kses_post |
'' |
Repeat for {n} = 1, 2, 3.
| Key pattern | Sanitizer | Default |
|---|---|---|
tisch_closing_{n}_from |
sanitize_text_field |
'' (YYYY-MM-DD) |
tisch_closing_{n}_to |
sanitize_text_field |
'' (YYYY-MM-DD) |
tisch_closing_{n}_label |
sanitize_text_field |
'' |
| Key | Sanitizer | Default |
|---|---|---|
tisch_tagesessen_pdf |
esc_url_raw |
'' |
tisch_tagesessen_pdf_id |
absint |
0 |
tisch_tagesessen_valid_until |
sanitize_text_field |
'' (YYYY-MM-DD) |
| Key | Sanitizer | Default |
|---|---|---|
tisch_speisekarte_pdf |
esc_url_raw |
'' |
tisch_speisekarte_pdf_id |
absint |
0 |
tisch_speisekarte_valid_until |
sanitize_text_field |
'' (YYYY-MM-DD) |
tisch_speisekarte_sections |
tisch_sanitize_speisekarte_sections |
[] |
tisch_speisekarte_sections is a nested PHP array: each element is [ 'title' => string, 'items' => [ [ 'name', 'price', 'desc', 'note' ], ... ] ].
| Key | Sanitizer | Default |
|---|---|---|
tisch_hero_tagline |
sanitize_text_field |
'Herzlich willkommen' |
tisch_hero_image_id |
absint |
0 |
tisch_welcome_text |
wp_kses_post |
'' |
| Key | Sanitizer | Default |
|---|---|---|
tisch_osm_lat |
tisch_sanitize_coordinate |
'48.087400' |
tisch_osm_lng |
tisch_sanitize_coordinate |
'9.218900' |
Stored as WordPress options via type => 'option' in the Customizer setting.
| Key | Sanitizer | Default |
|---|---|---|
tisch_color_primary |
sanitize_hex_color |
'#5C3D2E' |
tisch_color_primary_dark |
sanitize_hex_color |
'#3E2418' |
tisch_color_accent |
sanitize_hex_color |
'#C8922A' |
tisch_color_bg |
sanitize_hex_color |
'#FAF6F0' |
All functions are defined in inc/helpers.php.
tisch_tagesessen_is_valid(): boolReturns true if a Tagesessen PDF is set and the valid-until date has not passed. Returns false if no PDF is set. If no expiry date is set, always returns true.
tisch_speisekarte_is_valid(): boolSame logic as above but for the Speisekarte PDF option keys.
tisch_osm_embed_url(): stringBuilds the OpenStreetMap embed URL from the stored lat/lng values, with a fixed zoom bounding box and a marker pin.
tisch_get_opening_hours(): arrayReturns a 7-element array, one per weekday. Each element: [ 'label' => string, 'hours' => string, 'closed' => bool ].
tisch_get_active_closing(): arrayChecks today's date against all three closing-period slots. Returns [ 'from' => string, 'to' => string, 'label' => string ] for the matching period, or an empty array if none is active.
tisch_output_color_overrides(): voidHooked to wp_head at priority 99. Emits an inline <style> block overriding CSS custom properties with any custom hex colors saved via the Customizer.
tisch_phone_link(): stringReturns the stored phone number stripped to digits, +, and -, suitable for use in a tel: URI.
tisch_sanitize_speisekarte_sections( mixed $raw ): arraySanitizes the nested Speisekarte sections array from POST data. Removes sections that have neither a title nor any items.
inc/security.php applies the following hardening measures on every request:
- Version stripping — removes the WordPress version from the
<head>, RSS feeds, and all enqueued script/style query strings. - XML-RPC disabled —
xmlrpc_enabledfilter returns false; RSD and WLW manifest links removed from<head>. - User enumeration blocked —
/wp/v2/usersREST endpoints are removed for unauthenticated requests. - Head cleanup — removes shortlink, adjacent post rel links, feed discovery links, and oEmbed host/discovery tags from
<head>. - Embeds disabled — oEmbed route, filter, and rewrite rules are all removed.
- HTTP security headers — sends
X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN, andReferrer-Policy: strict-origin-when-cross-originon all front-end responses.
The reservation flow is handled entirely server-side with no database storage.
- The visitor fills out the form on the Reservierung page (name, email, phone, date, time, number of guests, optional message, DSGVO consent checkbox).
- On submit,
tisch_handle_reservation()runs on theinithook. - Nonce check —
wp_verify_nonce()againsttisch_reservation. Invalid nonce redirects to?reservierung=error. - Honeypot check — a hidden
tisch_websitefield that bots fill in. If non-empty, silently redirects to?reservierung=success(bot is not informed of detection). - Validation — name, valid email, date (YYYY-MM-DD format), time, guests ≥ 1, and DSGVO consent are all required. Missing fields redirect to
?reservierung=error. - Email —
wp_mail()sends a plain-text email to the address stored intisch_email, withReply-Toset to the guest's name and email. No HTML, no database row. - Redirect —
wp_safe_redirect()returns the visitor to the Reservierung page with a query param:
| Query param value | Displayed notice |
|---|---|
?reservierung=success |
Green success notice |
?reservierung=error |
Red error notice (validation or nonce failure) |
?reservierung=mail-error |
Red notice indicating the email could not be sent |
- Fonts — Playfair Display and Lato are served from
assets/fonts/as self-hosted.woff2files. No requests to Google Fonts or any other CDN. - OpenStreetMap — the map iframe on the Kontakt page is blocked until the visitor clicks "Karte anzeigen". Consent is stored in
localStorage(osm_consent=1) so subsequent visits load the map directly. - No analytics, no tracking pixels — the theme loads no third-party scripts of any kind.
- Reservation form — submitted data (name, email, phone, date, time, guests, message) is transmitted to the restaurant via
wp_mail()only. Nothing is written to the WordPress database.
Create a standard WordPress child theme that declares Template: tisch-kohler in its style.css. The child theme's functions.php is loaded before the parent; use it to add hooks or override functions.
To replace a template part without editing the parent theme, copy the file to the same relative path in your child theme. WordPress checks the child theme directory first.
For example, to replace the hero section:
child-theme/template-parts/home/hero.php
Add new keys to the $options array in inc/options.php and register them in tisch_register_settings() using register_setting( 'tisch_options_group', $key, $args ). Then render the field inside tisch_render_settings_page().
Initial release.
- Classic WordPress theme, no block editor requirement
- DSGVO-compliant: self-hosted fonts, OpenStreetMap consent gate, zero external scripts
- Custom admin settings page (contact, hours, closing periods, PDF upload, hero content, OSM coordinates)
- Customizer color overrides for four primary CSS variables
- Reservation form with nonce, honeypot, server-side validation,
wp_mail()delivery, no DB storage - Page templates for Tagesessen, Speisekarte (PDF + structured repeater), Catering, Reservierung, Über uns, Kontakt, Impressum, Datenschutzerklärung
- Security hardening: version stripping, XML-RPC disabled, user enumeration blocked, HTTP security headers