3 Commits
Author SHA1 Message Date
danielandClaude Sonnet 5 071ee344a3 Support one level of dropdown menus in the navbar
A `main` menu entry with child entries (others with `parent:` set) is
now rendered as a Bootstrap dropdown; flat entries are unchanged.
Active-state highlighting is trailing-slash tolerant and marks the
dropdown parent active when one of its children is the current page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 17:26:39 +02:00
danielandClaude Sonnet 5 ec9cba0555 Add lead shortcode; make banner a full-bleed hero image
- lead: wraps content in a Bootstrap .lead paragraph (moved from the
  ctsmockup site, where {.lead} could not target a paragraph).
- banner: resolves src as a page/global resource or base-URL-aware
  link, and spans the full viewport width via symmetric
  calc(50% - 50vw) margins while page content stays in .container.
  Optional height crops via object-fit: cover. A banner that starts a
  page's content is pulled flush against the navbar.
- html { overflow-x: hidden } to absorb the 100vw scrollbar overhang.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:53:20 +02:00
danielandClaude Sonnet 5 55308f7703 Respect base URL for nav logo; prefer logo.svg over favicon.svg
Reference the navbar brand image through relURL so it resolves
correctly when the site is served from a non-root base URL. Use
static/img/logo.svg when present, falling back to img/favicon.svg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:44:44 +02:00
5 changed files with 150 additions and 26 deletions

No files matched your search

+62
View File
@@ -3,6 +3,68 @@
Simple Bootstrap-based theme for Hugo.
## Shortcodes
### `lead`
Wraps its content in a Bootstrap `.lead` paragraph (the Markdown
attribute syntax `{.lead}` cannot target a paragraph):
```
{{< lead >}}
An introductory paragraph that stands out.
{{< /lead >}}
```
Pass extra classes with `class`, e.g. `{{< lead class="mb-5" >}}`.
### `banner`
A full-bleed banner / hero image: it spans the entire viewport width
while the rest of the page stays within `.container`. Put it on the
first line of a page's content to get a hero flush against the navbar.
```
{{< banner src="hero.png" alt="…" >}}
```
`src` is resolved as a page resource first, then a global resource
(`assets/`), then as a plain link respecting the site's base URL.
Optional params: `title`, `height` (a CSS length such as `60vh`, which
crops the image with `object-fit: cover`) and `class`.
## Navigation
The navbar renders the `main` menu. A top-level entry that has child
entries (other entries pointing at it with `parent:`) is rendered as a
Bootstrap dropdown; entries without children stay as plain links. Only
one level of nesting is supported (Bootstrap 5 has no nested submenus).
```yaml
# hugo.yaml / menus.yaml
menu:
main:
- name: Home
pageRef: /
weight: 1
- name: Research
weight: 2
- name: Proposals
parent: Research
pageRef: /proposals
weight: 1
- name: History
parent: Research
pageRef: /history
weight: 2
```
The entry matching the current page (and its parent, for a dropdown
child) gets Bootstrap's `active` class. A parent entry acts only as the
dropdown toggle, so it does not need its own `url`/`pageRef`.
## Custom error page
The theme has a custom 404 error page.
+22 -1
View File
@@ -15,6 +15,12 @@
--bs-body-line-height: 1.5;
}
// Keep the full-bleed `.banner` from adding a horizontal scrollbar
// (100vw is wider than the viewport when a vertical scrollbar is shown).
html {
overflow-x: hidden;
}
body {
font-family: var(--bs-font-sans-serif);
line-height: var(--bs-body-line-height);
@@ -61,8 +67,23 @@ h1, h2, h3, h4, h5 {
}
}
// Images
// Full-bleed banner / hero image: spans the viewport width while the
// surrounding page content stays within `.container`. Works from inside
// any centred `.container` because the horizontal margins cancel out the
// container offset (50% of the parent - 50% of the viewport).
.banner {
display: block;
width: 100vw;
max-width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
margin-bottom: 2rem;
}
// When the banner is the first thing in the page body, pull it flush
// against the navbar by cancelling the layout's top padding (`py-5`).
.content-body > .banner:first-child,
.content-body > p:first-child > .banner:only-child {
margin-top: -3rem;
}
+28 -21
View File
@@ -1,46 +1,53 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
<div class="container"> <!-- Changed from container-fluid to container -->
<a class="navbar-brand fw-bold" href="{{ .Site.BaseURL }}">
<img src="/img/favicon.svg" alt="{{ .Site.Title }}" title="{{ .Site.Title }}">
{{- $logo := "img/favicon.svg" -}}
{{- if os.FileExists "static/img/logo.svg" -}}{{- $logo = "img/logo.svg" -}}{{- end -}}
<img src="{{ $logo | relURL }}" alt="{{ .Site.Title }}" title="{{ .Site.Title }}">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false"
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{{ range .Site.Menus.main }}
{{ if .HasChildren }}
<!-- Dropdown menu for items with children -->
{{- /* Current page, without a trailing slash, for menu-item highlighting. */ -}}
{{- $current := strings.TrimSuffix "/" .RelPermalink -}}
{{- range .Site.Menus.main -}}
{{- $isActive := and .URL (eq (strings.TrimSuffix "/" .URL) $current) -}}
{{- if .HasChildren -}}
{{- /* A menu entry with sub-entries (set `parent:` on them) becomes a dropdown. */ -}}
{{- range .Children -}}
{{- if and .URL (eq (strings.TrimSuffix "/" .URL) $current) }}{{ $isActive = true }}{{ end -}}
{{- end -}}
{{- $id := printf "navbar-%s" (.Identifier | default (.Name | urlize)) -}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbar{{ .Name | urlize }}"
role="button" data-bs-toggle="dropdown" aria-expanded="false">
<a class="nav-link dropdown-toggle{{ if $isActive }} active{{ end }}" href="#"
id="{{ $id }}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ .Name }}
</a>
<ul class="dropdown-menu" aria-labelledby="navbar{{ .Name | urlize }}">
{{ range .Children }}
<ul class="dropdown-menu" aria-labelledby="{{ $id }}">
{{- range .Children }}
<li>
<a class="dropdown-item {{ if eq .URL ($.RelPermalink) }}active{{ end }}"
href="{{ .URL | relURL }}">
<a class="dropdown-item{{ if and .URL (eq (strings.TrimSuffix "/" .URL) $current) }} active{{ end }}"
href="{{ .URL }}">
{{ .Name }}
</a>
</li>
{{ end }}
{{- end }}
</ul>
</li>
{{ else }}
<!-- Regular menu item -->
{{- else -}}
<li class="nav-item">
<a class="nav-link {{ if eq .URL ($.RelPermalink) }}active{{ end }}"
href="{{ .URL | relURL }}">
<a class="nav-link{{ if $isActive }} active{{ end }}" href="{{ .URL }}">
{{ .Name }}
</a>
</li>
{{ end }}
{{ end }}
{{- end -}}
{{- end }}
</ul>
</div>
</div>
+36 -4
View File
@@ -1,4 +1,36 @@
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" }}
{{ $title := .Get "title" }}
<img class="banner img-fluid" src="{{ $src }}" alt="{{ $alt }}" title="{{ $title }}" width="100%">
{{- /*
Full-bleed banner / hero image.
The image spans the entire viewport width while the surrounding page
content stays within `.container` (see the `.banner` rules in
hugo_bootstrap_theme.scss). Place the shortcode as the first line of a
page's content to get a hero that sits flush against the navbar.
Params:
src image path - resolved as a page resource first, then a global
resource (assets/), then treated as a link/URL respecting the
site's base URL
alt alternative text (default: empty)
title optional title attribute
height optional CSS length (e.g. "60vh"); crops the image with
object-fit: cover
class optional extra CSS classes
*/ -}}
{{- $src := .Get "src" -}}
{{- $alt := or (.Get "alt") "" -}}
{{- $title := .Get "title" -}}
{{- $height := .Get "height" -}}
{{- $class := .Get "class" -}}
{{- $url := $src -}}
{{- with $.Page.Resources.GetMatch $src -}}
{{- $url = .RelPermalink -}}
{{- else -}}
{{- with resources.Get $src -}}
{{- $url = .RelPermalink -}}
{{- else -}}
{{- $url = $src | relURL -}}
{{- end -}}
{{- end -}}
<img class="banner{{ with $class }} {{ . }}{{ end }}" src="{{ $url }}" alt="{{ $alt }}"
{{- with $title }} title="{{ . }}"{{ end }}
{{- with $height }} style="height:{{ . }};object-fit:cover"{{ end }}>
+2
View File
@@ -0,0 +1,2 @@
{{ $additional_classes := .Get "class" }}
<p class="lead {{ $additional_classes }}">{{ .Inner | safeHTML }}</p>