11 Commits
Author SHA1 Message Date
danielandClaude Sonnet 5 13fb67a0f8 Add resource-tiles shortcode
Displays a page's file resources as a responsive grid of cards with a
thumbnail (explicit param, name-matched image, or a generic file icon),
title, size and download link. Meant for a downloads page offering a
handful of PDFs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 21:24:38 +02:00
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
danielandClaude Sonnet 5 fc8023961a Fall back to page or site Author param in footer copyright.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 13:40:30 +02:00
daniel d9febf84d8 Add scripts_extra.html dummy file. 2026-08-08 12:12:11 +02:00
daniel 6d3fb4613b Use 404.svg image if available. 2026-06-28 14:50:53 +02:00
daniel ba12b11f03 Add 404 page. 2026-06-28 14:08:17 +02:00
daniel b024424405 Add extra_head block. 2026-06-27 17:25:22 +02:00
daniel 0f3258f2db Remove whitespace in meta partial. 2026-06-24 16:10:29 +02:00
daniel 8d367dfa79 Add human file size partial. 2026-06-24 16:10:17 +02:00
12 changed files with 428 additions and 45 deletions

No files matched your search

+129
View File
@@ -1,3 +1,132 @@
# hugo_bootstrap_theme # hugo_bootstrap_theme
Simple Bootstrap-based theme for Hugo. 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`.
### `resource-tiles`
Displays a page's file resources as a responsive grid of cards
("tiles"), each with a thumbnail, a title, the file size and a download
link. Meant for a downloads page that offers a handful of PDFs,
spreadsheets, archives, …
```
{{< resource-tiles >}}
```
Params:
| Param | Default | Meaning |
|---------|---------|--------------------------------------------------------|
| `match` | `*.pdf` | glob selecting which page resources to show |
| `cols` | `3` | number of columns on large screens |
| `sort` | — | resource field to order by (`title`, `name`, `weight`) |
| `class` | — | extra CSS classes for the wrapping row |
Thumbnails are resolved per resource, in order of preference:
1. a `thumbnail` param on the resource, pointing at an image in the same
page bundle;
2. an image resource whose name matches the file, e.g. `report.png` or
`report.pdf.png` next to `report.pdf`;
3. a generic file icon showing the upper-cased extension.
Hugo cannot rasterise PDFs itself, so first-page previews have to be
produced as a build step. With poppler-utils installed, generating a
`<name>.png` next to every PDF under `content/` is a one-liner:
```sh
find content -name '*.pdf' -exec sh -c \
'pdftocairo -png -singlefile -scale-to-x 600 -scale-to-y -1 "$1" "${1%.pdf}"' _ {} \;
```
Run it before `hugo`; the tiles then use these as thumbnails
automatically. (`pdftoppm` takes the same flags; `mutool draw` is an
alternative.)
Per-resource title, description and thumbnail come from the page's front
matter:
```yaml
resources:
- src: report.pdf
title: Annual report
params:
description: Everything that happened last year.
thumbnail: report-cover.png
```
The whole tile is a click target (Bootstrap `.stretched-link`).
## 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.
If the site has an SVG image in `assets/svg/404.svg`,
this image will be displayed instead of a generic 404
code. Regardless of whether an SVG assets exists or not,
English and German explanations of what's going on will
be included on the page.
+55 -1
View File
@@ -15,6 +15,12 @@
--bs-body-line-height: 1.5; --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 { body {
font-family: var(--bs-font-sans-serif); font-family: var(--bs-font-sans-serif);
line-height: var(--bs-body-line-height); line-height: var(--bs-body-line-height);
@@ -61,8 +67,56 @@ 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 { .banner {
display: block;
width: 100vw;
max-width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
margin-bottom: 2rem; 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;
}
// Resource tiles (see the `resource-tiles` shortcode): a grid of cards,
// one per downloadable file, each with a thumbnail or a generic file
// icon on top.
.resource-tile {
&__thumb,
&__icon {
aspect-ratio: 3 / 2;
width: 100%;
object-fit: cover;
background-color: var(--bs-light);
}
&__icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: var(--bs-primary);
}
&__ext {
position: absolute;
bottom: 0.9rem;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
}
.card-title {
margin-bottom: 0.25rem;
}
}
+47
View File
@@ -0,0 +1,47 @@
{{ define "main" }}
<div class="container">
<div class="row">
<div class="col mt-5 mb-5">
{{ with resources.Get "svg/404.svg" }}
{{ .Content | safeHTML }}
{{ else }}
<h1 class="display-1 mx-auto text-center">
404
</h1>
{{ end }}
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="alert alert-danger mb-5" lang="en">
<h4 class="alert-heading">
Page not found
</h4>
<p>
The page you were trying to navigate to was not found on this
server.
</p>
<hr />
<p class="mb-0">
<a href="{{ .Site.BaseURL }}" class="alert-link">Back to the home page.</a>
</p>
</div>
</div>
<div class="col-lg-6">
<div class="alert alert-danger" lang="de">
<h4 class="alert-heading">
Seite nicht gefunden
</h4>
<p>
Die angeforderte Seite existiert nicht auf diesem Server.
</p>
<hr />
<p class="mb-0">
<a href="{{ .Site.BaseURL }}" class="alert-link">Zurück zur Homepage.</a>
</p>
</div>
</div>
</div>
</div>
{{ end }}
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="container py-4"> <div class="container py-4">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<p>&copy; {{ now.Year }} {{ .Site.Title }}</p> <p>&copy; {{ now.Year }} {{ or (or .Params.Author .Site.Params.Author) .Site.Title }}</p>
</div> </div>
<div class="col-md-6 text-md-end"> <div class="col-md-6 text-md-end">
<p> <p>
+24 -17
View File
@@ -1,7 +1,9 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top"> <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
<div class="container"> <!-- Changed from container-fluid to container --> <div class="container"> <!-- Changed from container-fluid to container -->
<a class="navbar-brand fw-bold" href="{{ .Site.BaseURL }}"> <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> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
@@ -12,35 +14,40 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
{{ range .Site.Menus.main }} {{- /* Current page, without a trailing slash, for menu-item highlighting. */ -}}
{{ if .HasChildren }} {{- $current := strings.TrimSuffix "/" .RelPermalink -}}
<!-- Dropdown menu for items with children --> {{- 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"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbar{{ .Name | urlize }}" <a class="nav-link dropdown-toggle{{ if $isActive }} active{{ end }}" href="#"
role="button" data-bs-toggle="dropdown" aria-expanded="false"> id="{{ $id }}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ .Name }} {{ .Name }}
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbar{{ .Name | urlize }}"> <ul class="dropdown-menu" aria-labelledby="{{ $id }}">
{{ range .Children }} {{- range .Children }}
<li> <li>
<a class="dropdown-item {{ if eq .URL ($.RelPermalink) }}active{{ end }}" <a class="dropdown-item{{ if and .URL (eq (strings.TrimSuffix "/" .URL) $current) }} active{{ end }}"
href="{{ .URL | relURL }}"> href="{{ .URL }}">
{{ .Name }} {{ .Name }}
</a> </a>
</li> </li>
{{ end }} {{- end }}
</ul> </ul>
</li> </li>
{{ else }} {{- else -}}
<!-- Regular menu item -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ if eq .URL ($.RelPermalink) }}active{{ end }}" <a class="nav-link{{ if $isActive }} active{{ end }}" href="{{ .URL }}">
href="{{ .URL | relURL }}">
{{ .Name }} {{ .Name }}
</a> </a>
</li> </li>
{{ end }} {{- end -}}
{{ end }} {{- end }}
</ul> </ul>
</div> </div>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
{{/* Call this with a resource's content e.g., partial "human_file_size" .Content */}}
{{- $bytes := len . -}}
{{- $kb := div $bytes 1024 -}}
{{- $mb := div $kb 1024 -}}
{{- if ge $mb 1 -}}{{- $mb -}} MB
{{- else if ge $kb 1 -}}{{- $kb -}} KB
{{- else -}}{{- $bytes -}} B{{- end -}}
+21 -22
View File
@@ -1,27 +1,26 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
<meta name="description" content="{{ .Page.Params.description }}"> <meta name="description" content="{{ .Page.Params.description }}">
<meta name="author" content="{{ .Site.Params.author }}"> <meta name="author" content="{{ or .Page.Params.author .Site.Params.author }}">
<meta name="generator" content="{{ hugo.Environment }}"> <meta name="generator" content="gohugo.io; {{ hugo.Environment }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/img/favicon.png"> <link rel="icon" type="image/png" href="/img/favicon.png">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
<!-- Compile SCSS to CSS --> <!-- Compile SCSS to CSS -->
{{ with resources.Get "scss/main.scss" }} {{- with resources.Get "scss/main.scss" -}}
{{ $opts := dict {{- $opts := dict
"enableSourceMap" (not hugo.IsProduction) "enableSourceMap" (not hugo.IsProduction)
"outputStyle" (cond hugo.IsProduction "compressed" "expanded") "outputStyle" (cond hugo.IsProduction "compressed" "expanded")
"targetPath" "css/main.css" "targetPath" "css/main.css"
"transpiler" "dartsass" "transpiler" "dartsass"
}} -}}
{{ with . | toCSS $opts }} {{- with . | toCSS $opts -}}
{{ if hugo.IsProduction }} {{- if hugo.IsProduction -}}
{{ with . | fingerprint "sha512" }} {{- with . | fingerprint "sha512" -}}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"> <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }} {{- end -}}
{{ else }} {{- else -}}
<link rel="stylesheet" href="{{ .RelPermalink }}"> <link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }} {{- end -}}
{{ end }} {{- end -}}
{{ end }} {{- end -}}
+2
View File
@@ -0,0 +1,2 @@
<!-- extra scripts may go here -->
+36 -4
View File
@@ -1,4 +1,36 @@
{{ $src := .Get "src" }} {{- /*
{{ $alt := .Get "alt" }} Full-bleed banner / hero image.
{{ $title := .Get "title" }}
<img class="banner img-fluid" src="{{ $src }}" alt="{{ $alt }}" title="{{ $title }}" width="100%"> 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>
+103
View File
@@ -0,0 +1,103 @@
{{- /*
resource-tiles — display a page's file resources as a responsive grid
of cards ("tiles"), each with a thumbnail, a title, the file size and a
download link. Handy for a downloads page that offers a handful of
PDFs, spreadsheets, archives, …
Params:
match glob selecting which page resources to show
(default "*.pdf")
cols number of columns on large screens (default 3)
sort resource field to order by: "title", "name" or "weight"
(default: the order they appear in front matter)
class extra CSS classes for the wrapping row
Thumbnails are resolved per resource, in order of preference:
1. a `thumbnail` param on the resource, pointing at an image in the
same page bundle
2. an image resource whose name matches the file, e.g. `report.png`
or `report.pdf.png` next to `report.pdf`
3. a generic file icon showing the upper-cased extension
Per-resource metadata is read from the page's front matter:
resources:
- src: report.pdf
title: Annual report
params:
description: Everything that happened last year.
thumbnail: report-cover.png
*/ -}}
{{- $page := .Page -}}
{{- $match := or (.Get "match") "*.pdf" -}}
{{- $cols := or (.Get "cols") 3 -}}
{{- $class := .Get "class" -}}
{{- $imgExts := slice "png" "jpg" "jpeg" "webp" "gif" "avif" -}}
{{- $resources := $page.Resources.Match $match -}}
{{- with .Get "sort" -}}
{{- $resources = sort $resources . -}}
{{- end -}}
{{- with $resources -}}
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-{{ $cols }} g-4 my-4{{ with $class }} {{ . }}{{ end }}">
{{- range . }}
{{- $res := . -}}
{{- $ext := strings.TrimPrefix "." (path.Ext .Name) -}}
{{- /* Title: front-matter `title`, else the humanized file name.
Hugo falls back .Title to .Name, so treat that as unset. */ -}}
{{- $title := .Title -}}
{{- if or (not $title) (eq $title .Name) -}}
{{- $title = humanize (path.BaseName .Name) -}}
{{- end -}}
{{- /* Resolve a thumbnail image (see the doc comment above). */ -}}
{{- $thumb := false -}}
{{- with .Params.thumbnail -}}
{{- $thumb = $page.Resources.GetMatch . -}}
{{- end -}}
{{- if not $thumb -}}
{{- $base := path.BaseName $res.Name -}}
{{- range $imgExts -}}
{{- if not $thumb -}}
{{- $thumb = or ($page.Resources.GetMatch (printf "%s.%s" $base .)) ($page.Resources.GetMatch (printf "%s.%s" $res.Name .)) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if and $thumb (eq $thumb.ResourceType "image") -}}
{{- $thumb = $thumb.Fill "600x400 Center" -}}
{{- end -}}
<div class="col">
<div class="card resource-tile h-100">
{{- if $thumb }}
<img class="card-img-top resource-tile__thumb" src="{{ $thumb.RelPermalink }}" alt="{{ $title }}" loading="lazy">
{{- else }}
<div class="card-img-top resource-tile__icon" aria-hidden="true">
<svg width="72" height="72" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2h8l4 4v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z"
fill="currentColor" fill-opacity=".08" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M14 2v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
<span class="resource-tile__ext">{{ $ext | upper }}</span>
</div>
{{- end }}
<div class="card-body">
<h5 class="card-title">{{ $title }}</h5>
{{- with $res.Params.description }}
<p class="card-text">{{ $page.RenderString . }}</p>
{{- end }}
</div>
<div class="card-footer bg-transparent d-flex align-items-center justify-content-between gap-2">
<span class="text-body-secondary small text-uppercase">
{{- $ext }}{{ with $res.Content }} &middot; {{ partial "human_file_size" . }}{{ end -}}
</span>
<a class="btn btn-primary btn-sm stretched-link" href="{{ $res.RelPermalink }}" download>Download</a>
</div>
</div>
</div>
{{- end }}
</div>
{{- end -}}
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
{{- partial "meta.html" . -}} {{- partial "meta.html" . -}}
{{- partial "head_extra.html" . -}} {{- partial "head_extra.html" . -}}
{{- block "extra_head" . }}{{ end }}
</head> </head>
<body> <body>
{{- partial "header.html" . -}} {{- partial "header.html" . -}}