feat(slides): add persistent branding with per-slide visibility control

- Add branding overlays: logo, title, author, and footer text
- Use Reveal.js theme CSS variables for automatic color adaptation
- Inline SVGs for currentColor support on any theme
- Per-slide control via HTML comments:
  - <!-- no-branding -->: hide all elements
  - <!-- no-header -->: hide logo + title
  - <!-- no-footer -->: hide author + footer
- Fix body-level CSS selectors for proper visibility
- Add comprehensive documentation in example slides
This commit is contained in:
George Cushen committed 2026-01-13 15:36:32 +00:00
1 parent 1352d89867
commit aa9edea5df
16 files changed
+902 -17

No files matched your search

@@ -0,0 +1,157 @@
{{/*
Slide Branding Component
Renders persistent branding elements (logo, title overlay, etc.) that appear
across all slides in a presentation.
Configuration (site.Params.slides.branding or .Params.slides.branding):
logo:
src: "media/logo.png" # Path to logo image
alt: "Organization Logo" # Alt text
position: "top-left" # top-left, top-right, bottom-left, bottom-right
width: "100px" # Logo width
margin: "20px" # Distance from edges
title:
show: true # Show presentation title
position: "bottom-left" # Position of title text
author:
show: true # Show author/presenter name
position: "bottom-right" # Position of author text
*/}}
{{/* Merge page-level config with site-level defaults */}}
{{ $site_branding := site.Params.slides.branding | default dict }}
{{ $page_branding := .Params.slides.branding | default dict }}
{{ $branding := merge $site_branding $page_branding }}
{{/* Logo rendering */}}
{{ with $branding.logo }}
{{ if .filename }}
{{ $resource_path := printf "media/%s" .filename }}
{{ $logo_resource := resources.Get $resource_path }}
{{ with $logo_resource }}
{{ $position := $branding.logo.position | default "top-left" }}
{{ $width := $branding.logo.width | default "80px" }}
{{ $margin := $branding.logo.margin | default "20px" }}
{{ $alt := $branding.logo.alt | default "Logo" }}
{{/* Calculate CSS position based on position string */}}
{{ $positionCSS := "" }}
{{ if eq $position "top-left" }}
{{ $positionCSS = printf "top: %s; left: %s;" $margin $margin }}
{{ else if eq $position "top-right" }}
{{ $positionCSS = printf "top: %s; right: %s;" $margin $margin }}
{{ else if eq $position "bottom-left" }}
{{ $positionCSS = printf "bottom: %s; left: %s;" $margin $margin }}
{{ else if eq $position "bottom-right" }}
{{ $positionCSS = printf "bottom: %s; right: %s;" $margin $margin }}
{{ else }}
{{/* Custom position - expect CSS string */}}
{{ $positionCSS = $position }}
{{ end }}
<div id="slide-logo" style="{{ $positionCSS | safeCSS }} width: {{ $width }};" aria-label="{{ $alt }}">
{{/* Inline SVGs to enable currentColor, use img for other formats */}}
{{ if strings.HasSuffix .Name ".svg" }}
{{ .Content | safeHTML }}
{{ else }}
<img src="{{ .RelPermalink }}" alt="{{ $alt }}">
{{ end }}
</div>
{{ end }}
{{ end }}
{{ end }}
{{/* Title overlay (optional) */}}
{{ if $branding.title }}
{{ with $branding.title }}
{{ if .show }}
{{ $position := .position | default "bottom-left" }}
{{ $margin := .margin | default "20px" }}
{{ $positionCSS := "" }}
{{ if eq $position "top-left" }}
{{ $positionCSS = printf "top: %s; left: %s;" $margin $margin }}
{{ else if eq $position "top-right" }}
{{ $positionCSS = printf "top: %s; right: %s;" $margin $margin }}
{{ else if eq $position "bottom-left" }}
{{ $positionCSS = printf "bottom: %s; left: %s;" $margin $margin }}
{{ else if eq $position "bottom-right" }}
{{ $positionCSS = printf "bottom: %s; right: %s;" $margin $margin }}
{{ end }}
<div id="slide-title-overlay" style="{{ $positionCSS | safeCSS }}">
{{ .text | default $.Title }}
</div>
{{ end }}
{{ end }}
{{ end }}
{{/* Author overlay (optional) */}}
{{ if $branding.author }}
{{ with $branding.author }}
{{ if .show }}
{{ $position := .position | default "bottom-right" }}
{{ $margin := .margin | default "20px" }}
{{ $positionCSS := "" }}
{{ if eq $position "top-left" }}
{{ $positionCSS = printf "top: %s; left: %s;" $margin $margin }}
{{ else if eq $position "top-right" }}
{{ $positionCSS = printf "top: %s; right: %s;" $margin $margin }}
{{ else if eq $position "bottom-left" }}
{{ $positionCSS = printf "bottom: %s; left: %s;" $margin $margin }}
{{ else if eq $position "bottom-right" }}
{{ $positionCSS = printf "bottom: %s; right: %s;" $margin $margin }}
{{ end }}
{{/* Get author from page params or first author */}}
{{ $author := "" }}
{{ with $.Params.authors }}
{{ if reflect.IsSlice . }}
{{ $author = index . 0 }}
{{ else }}
{{ $author = . }}
{{ end }}
{{ end }}
{{ with $.Params.author }}
{{ $author = . }}
{{ end }}
{{ if $author }}
<div id="slide-author-overlay" style="{{ $positionCSS | safeCSS }}">
{{ $author }}
</div>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* Footer text overlay (optional) - e.g. for copyright */}}
{{ if $branding.footer }}
{{ with $branding.footer }}
{{ if .text }}
{{ $position := .position | default "bottom-center" }}
{{ $margin := .margin | default "20px" }}
{{ $positionCSS := "" }}
{{ if eq $position "top-left" }}
{{ $positionCSS = printf "top: %s; left: %s;" $margin $margin }}
{{ else if eq $position "top-right" }}
{{ $positionCSS = printf "top: %s; right: %s;" $margin $margin }}
{{ else if eq $position "bottom-left" }}
{{ $positionCSS = printf "bottom: %s; left: %s;" $margin $margin }}
{{ else if eq $position "bottom-right" }}
{{ $positionCSS = printf "bottom: %s; right: %s;" $margin $margin }}
{{ else if eq $position "bottom-center" }}
{{ $positionCSS = printf "bottom: %s; left: 50%%; transform: translateX(-50%%);" $margin }}
{{ end }}
<div id="slide-footer-text-overlay" style="{{ $positionCSS | safeCSS }}">
{{ .text | markdownify }}
</div>
{{ end }}
{{ end }}
{{ end }}
@@ -0,0 +1,39 @@
{{/*
Slides Hook Loader - Independent Implementation
This is a self-contained hook system for the slides module that doesn't
depend on the blox module. Users can create partials in their own project's
layouts/_partials/hooks/{hook-name}/ directory.
Usage: {{ partial "functions/slides_get_hook" (dict "hook" "slide-header" "context" .) }}
Available hooks for slides:
- slide-header: Top of presentation (inside .reveal, above .slides)
- slide-footer: Bottom of presentation (inside .reveal, below .slides)
- slide-head-end: End of <head> tag (for custom CSS/JS)
- slide-body-end: End of <body> tag (for custom JS)
Input: dict with "hook" (string) and "context" (page context)
Output: Renders all partials found in the hook directory
*/}}
{{ $loaded := false }}
{{ $partial_dir := printf "hooks/%s/" .hook }}
{{ $context := .context }}
{{ $hook_dir_path := path.Join "layouts/_partials" $partial_dir }}
{{/* Use try to gracefully handle missing directories */}}
{{ with try (os.ReadDir $hook_dir_path) }}
{{ with .Value }}
{{ range . }}
{{ if not .IsDir }}
{{ $partial_path := path.Join $partial_dir .Name }}
{{ partial $partial_path $context }}
{{ $loaded = true }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* Debug: uncomment to see if hooks are loaded */}}
{{/* return $loaded */}}
@@ -0,0 +1,22 @@
{{/*
Example Slide Body-End Hook
This hook allows injecting custom JavaScript at the end of the <body>
tag, after Reveal.js has been loaded. Useful for:
- Custom Reveal.js plugins
- Analytics tracking on slide changes
- Custom keyboard shortcuts
- Integration with other JS libraries
To use this hook, create a file at:
layouts/_partials/hooks/slide-body-end/your-custom-script.html
*/}}
{{/* Uncomment below to track slide changes with console logging */}}
{{/*
<script>
Reveal.on('slidechanged', event => {
console.log('Slide changed to:', event.indexh, event.indexv);
});
</script>
*/}}
@@ -0,0 +1,20 @@
{{/*
Example Slide Footer Hook
This is an example file showing how to create custom footer content
that appears at the bottom of every slide in a presentation.
To use this hook, create a file at:
layouts/_partials/hooks/slide-footer/your-custom-footer.html
The content will be rendered inside a <div class="slide-footer"> container.
Example usage: show social media handle or contact info
*/}}
{{/* Uncomment below to show author's social handle in footer */}}
{{/*
<div style="font-size: 0.5em; color: rgba(255,255,255,0.5); padding: 10px; text-align: right; width: 100%;">
@yourhandle
</div>
*/}}
@@ -0,0 +1,25 @@
{{/*
Example Slide Head-End Hook
This hook allows injecting custom CSS or JavaScript into the <head>
section of the presentation. Useful for:
- Custom fonts
- Additional CSS frameworks
- Analytics scripts
- Custom Reveal.js configurations
To use this hook, create a file at:
layouts/_partials/hooks/slide-head-end/your-custom-head.html
*/}}
{{/* Uncomment below to add custom font */}}
{{/*
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet">
<style>
.reveal pre code {
font-family: 'Fira Code', monospace;
}
</style>
*/}}
@@ -0,0 +1,30 @@
{{/*
Example Slide Header Hook
This is an example file showing how to create custom header content
that appears at the top of every slide in a presentation.
To use this hook, create a file at:
layouts/_partials/hooks/slide-header/your-custom-header.html
The content will be rendered inside a <div class="slide-header"> container.
Available context variables:
.Title - Presentation title
.Params - All front matter parameters
.Params.authors - List of authors
.Params.course - Course code/name
.Params.venue - Event venue
.Date - Presentation date
Example usage:
*/}}
{{/* Uncomment below to show course code in header */}}
{{/*
{{ with .Params.course }}
<div style="font-size: 0.6em; color: rgba(255,255,255,0.6); padding: 10px;">
{{ . }}{{ with $.Params.lecture_number }} · Lecture {{ . }}{{ end }}
</div>
{{ end }}
*/}}
+31 -1
View File
@@ -26,7 +26,20 @@
{{/* Each `<section>` defines a new slide. */}}
{{/* Only begin new slide `<section>` if not already added by custom `slide` shortcode. */}}
{{ if not (in . "data-noprocess") }}
<section{{ if $isHidden }} data-visibility="hidden"{{ end }}>
{{/* Parse visibility comments */}}
{{ $state := slice }}
{{ if in . "<!-- no-branding -->" }}
{{ $state = $state | append "no-branding" }}
{{ end }}
{{ if in . "<!-- no-header -->" }}
{{ $state = $state | append "no-header" }}
{{ end }}
{{ if in . "<!-- no-footer -->" }}
{{ $state = $state | append "no-footer" }}
{{ end }}
<section{{ if $isHidden }} data-visibility="hidden"{{ end }} {{ if gt (len $state) 0 }}data-state="{{ delimit $state " " }}"{{ end }}>
{{ end }}
{{ $slideContent | safeHTML }}
</section>
@@ -35,4 +48,21 @@
{{ end }}
{{ end }}
</div>
{{/* Branding elements - placed inside .reveal but outside .slides to be persistent and fixed */}}
{{/* Branding overlay (logo, title, author) */}}
{{ $pageContext := index . 0 }}
{{ partial "components/slide-branding" $pageContext }}
{{/* Hook: slide-header */}}
<div class="slide-header">
{{ partial "functions/slides_get_hook" (dict "hook" "slide-header" "context" $pageContext) }}
</div>
{{/* Hook: slide-footer */}}
<div class="slide-footer">
{{ partial "functions/slides_get_hook" (dict "hook" "slide-footer" "context" $pageContext) }}
</div>
</div>
@@ -1,5 +1,23 @@
{{/*
Present Baseof - Presentation output only
Hook Points Available:
- slide-head-end: End of <head> for custom CSS/JS
- slide-header: Top of presentation (inside .reveal wrapper)
- slide-footer: Bottom of presentation (inside .reveal wrapper)
- slide-body-end: End of <body> for custom JS
Branding Configuration (site.Params.slides.branding or page front matter):
logo:
src: "media/logo.png"
position: "top-left" # top-left, top-right, bottom-left, bottom-right
width: "100px"
title:
show: true
position: "bottom-left"
author:
show: true
position: "bottom-right"
*/}}
<!doctype html>
@@ -38,6 +56,13 @@
{{- $theme := $.Param "slides.theme" | default "black" -}}
<link rel="stylesheet" href="{{ $cdn_url_reveal }}/dist/theme/{{ $theme }}.min.css">
{{/* Load branding CSS */}}
{{ $branding_css := resources.Get "css/slides-branding.css" }}
{{ with $branding_css }}
{{ $branding_css = . | minify | fingerprint }}
<link rel="stylesheet" href="{{ $branding_css.RelPermalink }}">
{{ end }}
{{/* Hugo Chroma Syntax Highlighter */}}
{{ $hl_theme := $.Param "slides.highlight_style" | default "dracula" }}
{{ $hl_theme_path := printf "css/libs/chroma/%s.css" $hl_theme }}
@@ -58,6 +83,10 @@
document.head.appendChild(link);
})();
</script>
{{/* Hook: slide-head-end - Custom CSS/JS injection point */}}
{{ partial "functions/slides_get_hook" (dict "hook" "slide-head-end" "context" .) }}
</head>
<body>
@@ -80,7 +109,8 @@
{{ $slidejs := resources.Get "js/hugoblox-slides.js" | js.Build (dict "params" (dict "slides" $.Params.slides )) | fingerprint }}
<script src="{{ $slidejs.RelPermalink }}"></script>
{{ partial "functions/get_hook" (dict "hook" "body-end" "context" .) }}
{{/* Hook: slide-body-end - End of body for custom JS */}}
{{ partial "functions/slides_get_hook" (dict "hook" "slide-body-end" "context" .) }}
</body>
</html>