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.