Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13fb67a0f8 | ||
|
|
071ee344a3 | ||
|
|
ec9cba0555 | ||
|
|
55308f7703 |
No files matched your search
@@ -3,6 +3,123 @@
|
|||||||
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
|
## Custom error page
|
||||||
|
|
||||||
The theme has a custom 404 error page.
|
The theme has a custom 404 error page.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{{ $additional_classes := .Get "class" }}
|
||||||
|
<p class="lead {{ $additional_classes }}">{{ .Inner | safeHTML }}</p>
|
||||||
@@ -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 }} · {{ 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 -}}
|
||||||
Reference in new issue
Block a user