Skip to main content

Content Authoring

Pages are HTML files with a YAML frontmatter block. The binary scans a content directory at startup, parses every file, and registers a route for each page slug.

Directory structure

content/
de/
home.html → slug: "home", lang: de
kontakt.html → slug: "kontakt", lang: de
en/
home.html → slug: "home", lang: en
contact.html → slug: "contact", lang: en
  • The language code is the directory name (de, en, fr, …).
  • The slug is the filename without .html.
  • For multilingual sites, each language directory must contain a file for every page.

Frontmatter

Every file starts with a YAML frontmatter block delimited by ---:

---
id: contact
template: contact
title: "Contact"
description: "Get in touch with us."
---

<!-- body HTML here -->

Reserved frontmatter keys

KeyTypeRequiredDescription
idstringrecommendedInternal page identifier, shared across languages. Defaults to the slug if omitted.
templatestringnoTemplate to render. One of home, default, contact, legal. Defaults to default.
titlestringyesPage title used in <title> tag (unless seo_title overrides it).
descriptionstringyesMeta description (unless seo_description overrides it).
keywordsstringnoMeta keywords (comma-separated).
startPageboolnoMark this page as the start page for its language. Root / redirects here. Set on exactly one page per language.
seo_titlestringnoOverrides title in <title>, og:title. See SEO.
seo_descriptionstringnoOverrides description in meta description and og:description.
seo_priorityfloatnoSitemap priority (0.0–1.0). Default: 0.5.
seo_changefreqstringnoSitemap change frequency. Default: monthly.
seo_ogimagestringnoAbsolute path or URL for og:image.
seo_ogtypestringnoOG type. Defaults to website for home/legal, article for others.
seo_noindexboolnoAdd <meta name="robots" content="noindex, nofollow">.

All other keys are passed through to the template as content values accessible via data.Content("key").

Content keys (template-specific)

Keys not in the reserved list above become available in templates as data.Content("key"). See Templates for which keys each template uses.

---
id: home
template: home
title: "Home"
description: "Welcome to my site."
startPage: true
heroLine1: "Simple."
heroLine2: "Fast."
heroSubtitle: "Built with webhull."
heroCTA1Text: "Get started"
heroCTA1Link: "/docs"
---

Body formats

The HTML after the closing --- delimiter is the page body. Three formats are supported — pick one per file.

Plain body

The entire body is accessible as data.Content("body") in the template:

---
id: about
template: default
title: "About"
description: "About this project."
heroTitle: "About"
---

<p>This is the about page.</p>
<p>Any HTML is valid here.</p>

Named sections (legacy)

Split the body into named sections using <!-- section: name --> markers:

---
id: about
template: default
title: "About"
description: "About this project."
---

<!-- section: body -->
<p>Main content here.</p>

<!-- section: sidebar -->
<p>Sidebar content here.</p>

Each section is accessible in the template as data.Content("body"), data.Content("sidebar"), etc.

For structured layouts — use <!-- section[type,options]: Title --> markers. Sections are rendered in declaration order by the template.

---
id: services
template: default
title: "Services"
description: "What we offer."
heroTitle: "Services"
---

<!-- section[block]: Our Approach -->
<p>We focus on long-term partnerships.</p>

<!-- section[grid,altbg]: What We Offer -->
<div class="card">
<h3>Consulting</h3>
<p>Architecture and strategy.</p>
</div>
<div class="card">
<h3>Development</h3>
<p>From prototype to production.</p>
</div>

<!-- section[services,altbg,id=tech]: Technology -->
<div class="service-card">...</div>

Marker syntax:

<!-- section[type]: Title HTML -->
<!-- section[type,altbg]: Title HTML -->
<!-- section[type,altbg,id=anchor]: Title HTML -->
PartValuesDescription
typeblock, grid, servicesControls CSS layout class applied to the section
altbgflag (no value)Applies alt-bg CSS modifier for alternating background
id=anchorstringSets the HTML id on the section element (for anchor links)
TitleHTML stringRendered as <h2> inside the section header. Empty = no header.

Typed sections are available in data.Page.Sections as an ordered slice.

Multilingual pages

Pages are matched across languages by their id field. If all language files share the same id, webhull automatically generates hreflang alternate links in the <head>.

content/de/contact.html → id: contact → slug: kontakt
content/en/contact.html → id: contact → slug: contact

The id ties the two files together. The slugs can differ per language.

If a language file is missing for a configured language, startup fails with:

page "contact" missing i18n for language "en"

Validation at startup

webhull validates all content at startup:

  • Every page must have an entry for every configured language.
  • No two pages may share the same slug within a language.
  • Every language must have exactly one page with startPage: true.