Skip to main content

Configuration Reference

Complete reference for all fields in site.yaml (monolithic) or config.yaml + pages.yaml (split config).

All string values support ${VAR:default} environment variable expansion. See Environment Variables for a full list.


site — Site identity

site:
name: "Acme Corp"
baseURL: "https://example.com"
logoPath: "/static/img/logo.webp"
faviconPath: "/static/img/favicon.webp"
copyrightStartYear: 2022
showLangFlags: false
FieldTypeDefaultDescription
namestringRequired. Site name used in <meta name="author">, gate page title, and startup log.
baseURLstringRequired. Absolute base URL without trailing slash. Used for canonical URLs, OG tags, and sitemap <loc>.
logoPathstringPath to the site logo shown in the header.
faviconPathstringPath to the favicon (<link rel="icon">).
copyrightStartYearintcurrent yearStart year for the copyright range in the footer (e.g. 2022 – 2026).
showLangFlagsboolfalseShow flag icons in the language switcher.

i18n — Internationalisation

i18n:
defaultLanguage: "en"
languages: ["en", "de"]
FieldTypeDefaultDescription
defaultLanguagestringRequired. Language code for the default language. Used for hreflang="x-default" and language detection fallback.
languages[]stringRequired. All supported language codes. Each language must have a content directory.

contentDir — Content directory

contentDir: "content"

Path to the directory containing HTML content files, relative to the config file. Structure: contentDir/{lang}/*.html. When set, webhull scans the directory at startup and registers a route for every file found.


navigation:
header:
en:
- title: "About"
url: "/about"
slug: "about"
children:
- title: "Team"
url: "/team"
slug: "team"
footer:
en:
- title: "Legal"
url: "#"
slug: "legal"
children:
- title: "Imprint"
url: "/imprint"
slug: "imprint"

Navigation is defined per language. Each item has title, url, slug (for active-state matching), and optional children.


ui — Per-language UI strings

ui:
en:
contactURL: "/contact"
contactLabel: "Contact"
footerTagline: "Built with care."
allRights: "All rights reserved."
imprintURL: "/imprint"
imprintLabel: "Imprint"
privacyURL: "/privacy"
privacyLabel: "Privacy"
notFoundTitle: "Page not found"
notFoundSubtitle: "The page you are looking for does not exist."
notFoundButton: "Back to home"
contactForm:
heading: "Send us a message"
nameLabel: "Name"
emailLabel: "Email"
subjectLabel: "Subject"
messageLabel: "Message"
submitText: "Send"
successMsg: "Thank you! We will be in touch."
successRefMsg: "Your reference:"
errorMsg: "Something went wrong. Please try again."
requiredMsg: "Please fill in all required fields."
FieldDescription
contactURLURL for the contact CTA button in the header.
contactLabelLabel for the contact CTA button.
footerTaglineTagline text in the footer.
allRights"All rights reserved" text in the footer.
imprintURL / imprintLabelImprint link in the footer.
privacyURL / privacyLabelPrivacy policy link in the footer.
notFoundTitleH1 on the 404 page.
notFoundSubtitleSubtitle on the 404 page.
notFoundButtonBack-to-home button label on the 404 page.
contactForm.*Contact form label overrides. All optional — fall back to built-in DE/EN defaults. See Contact Form.

seo — SEO defaults

seo:
defaultOGImage: "/static/img/og/default.webp"
defaultTwitterCard: "summary_large_image"
globalJsonLD:
- |
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Corp",
"url": "https://example.com"
}
FieldTypeDefaultDescription
defaultOGImagestringFallback og:image for pages without seo_ogimage. Relative paths are prefixed with site.baseURL.
defaultTwitterCardstringsummary_large_imageDefault Twitter Card type.
globalJsonLD[]stringRaw JSON-LD blocks injected on every page. Use for Organization, WebSite schemas.

server — HTTP server

server:
port: "${PORT:8080}"
host: "0.0.0.0"
environment: "${ENVIRONMENT:development}"
readTimeout: 15s
writeTimeout: 15s
idleTimeout: 60s
shutdownTimeout: 30s
staticDir: "static"
cacheMaxAge: 1h
staticCacheMaxAge: 8760h
FieldTypeDefaultDescription
portstring8080HTTP listen port.
hoststring0.0.0.0HTTP listen address.
environmentstringdevelopmentdevelopment enables Gin debug mode and dev logger. production enables release mode and JSON logger.
readTimeoutduration15sHTTP read timeout.
writeTimeoutduration15sHTTP write timeout.
idleTimeoutduration60sHTTP keep-alive idle timeout.
shutdownTimeoutduration30sGraceful shutdown deadline on SIGINT/SIGTERM.
staticDirstringPath to the static files directory. Served at /static/*.
cacheMaxAgedurationCache-Control: max-age for rendered HTML pages.
staticCacheMaxAgedurationCache-Control: max-age for static assets. Typically set to 1 year (8760h) — cache-busting is handled via content-hash query params.

health — Health server

A separate HTTP server for Kubernetes liveness, readiness, and metrics probes. Runs on a different port from the main server so probes are never blocked by application middleware.

health:
enabled: true
host: "0.0.0.0"
port: 9090
healthPath: "/health"
readyPath: "/ready"
metricsPath: "/metrics"
timeout: 5s
serviceName: "my-site"
serviceVersion: "1.2.3"
FieldTypeDefaultDescription
enabledboolfalseEnable the health server.
hoststring0.0.0.0Listen address.
portintListen port. Required when enabled.
healthPathstring/healthLiveness probe path. Returns {"status":"ok"}.
readyPathstring/readyReadiness probe path. Returns {"status":"ready"}.
metricsPathstring/metricsPrometheus-style metrics path. Exposes webhull_info and webhull_uptime_seconds.
timeoutdurationRead/write/idle timeout for the health server.
serviceNamestringwebhullValue of the service label in webhull_info.
serviceVersionstringunknownValue of the version label in webhull_info.

contact — Contact form

contact:
enabled: true
recipientName: "Support"
recipients:
- "hello@example.com"
subject:
en: "New message from {{.Name}}"
de: "Neue Nachricht von {{.Name}}"
maxLinks: 2
rateLimit:
requests: 3
window: 15m
FieldTypeDefaultDescription
enabledboolfalseEnable the contact form and /api/contact endpoint.
recipientNamestringDisplay name in the To: header of outgoing emails.
recipients[]stringEmail addresses to deliver form submissions to.
subjectmap[string]stringPer-language email subject template. Variables: {{.Name}}, {{.Subject}}.
maxLinksint2Messages containing more URLs than this are rejected as spam.
rateLimit.requestsint3Maximum form submissions per window per IP.
rateLimit.windowduration15mRate limit window.

See Contact Form for auto-reply templates and label customisation.


mail — SMTP

mail:
host: "${SMTP_HOST:smtp.example.com}"
port: 587
username: "${SMTP_USERNAME}"
password: "${SMTP_PASSWORD}"
from: "noreply@example.com"
fromName: "Acme Corp"
useTLS: false
templates:
en:
subject: "We received your message"
body: "<p>Thank you, {{.Name}}.</p>"
de:
subject: "Ihre Nachricht ist angekommen"
bodyFile: "mail/auto-reply-de.html"
FieldTypeDefaultDescription
hoststringSMTP server hostname.
portint587SMTP port. Use 587 for STARTTLS, 465 for TLS-on-connect.
usernamestringSMTP authentication username.
passwordstringSMTP authentication password.
fromstringSender email address.
fromNamestringSender display name.
useTLSboolfalseUse direct TLS (port 465). false = STARTTLS (port 587).
templatesmapPer-language auto-reply templates. body = inline HTML; bodyFile = path to HTML file (relative to config dir).

analytics — Analytics providers

analytics:
plausible:
enabled: true
domain: "example.com"
baseURL: "https://plausible.io"
scriptPath: "/js/script.js"
collector:
enabled: false
endpoint: "https://collector.example.com/events"
FieldTypeDefaultDescription
plausible.enabledboolfalseEnable Plausible proxy.
plausible.domainstringPlausible site domain (data-domain attribute).
plausible.baseURLstringPlausible server base URL.
plausible.scriptPathstring/js/script.jsPath to the tracking script on the Plausible server.
collector.enabledboolfalseEnable custom collector proxy.
collector.endpointstringHTTP endpoint to forward events to.

consent:
enabled: true
categories:
necessary:
required: true
default: true
analytics:
required: false
default: false
i18n:
en:
title: "Cookie Settings"
description: "We use cookies for website operation and analytics."
acceptAll: "Accept all"
rejectAll: "Reject all"
customize: "Customize"
save: "Save preferences"
categories:
necessary: "Necessary"
analytics: "Analytics"
FieldTypeDescription
enabledboolShow the consent banner on first visit.
categories.<name>.requiredboolRequired categories cannot be toggled off.
categories.<name>.defaultboolDefault state before the visitor decides.
i18n.<lang>.titlestringBanner title.
i18n.<lang>.descriptionstringBanner description.
i18n.<lang>.categories.<name>stringPer-category display name in the banner.

gate — Site-wide access gate

gate:
enabled: true
cookieSecret: "${GATE_SECRET}"
cookieName: "gate_session"
cookieMaxAge: 24h
codes:
- code: "aurora-pine-7"
label: "Prospect — Acme Corp"
msgTooManyAttempts: "Too many attempts. Please try again later."
msgInvalidCode: "Invalid access code. Please try again."
FieldTypeDefaultDescription
enabledboolfalseProtect the entire site behind a code gate.
cookieSecretstringRequired when enabled. HMAC-SHA256 signing key. Min 32 chars.
cookieNamestringgate_sessionSession cookie name.
cookieMaxAgeduration24hSession cookie lifetime.
codes[]GateCodeRequired when enabled. List of valid access codes.
codes[].codestringPlaintext access code.
codes[].labelstringInternal label (never shown to visitors).
msgTooManyAttemptsstringEnglish defaultError shown after rate limit is exceeded.
msgInvalidCodestringEnglish defaultError shown for wrong codes.

See Access Gate for full documentation.


arconGate — Path-scoped access gate

Protects only /arcon/*. All other pages remain public.

arconGate:
enabled: true
contentDir: "arcon"
cookieSecret: "${ARCON_GATE_SECRET}"
cookieName: "arcon_session"
cookieMaxAge: 8h
title: "My Product – Access"
codes:
- code: "cedar-frost-9"
label: "Demo prospect"
FieldTypeDefaultDescription
enabledboolfalseEnable the arcon gate.
contentDirstringRequired when enabled. Directory served at /arcon/*.
cookieSecretstringRequired when enabled. HMAC-SHA256 signing key.
cookieNamestringarcon_sessionSession cookie name.
cookieMaxAgeduration8hSession cookie lifetime.
titlestring<site.name> – AccessGate page browser title.
codes[]GateCodeRequired when enabled. List of valid access codes.