Rendering Learning Hub with Hugo: Practical Implementation Guide
Rendering Learning Hub with Hugo
π Overview
This practical guide demonstrates how to configure Hugo to render the Learning Hub documentation site (currently built with Quarto), covering content structure mapping, theme configuration, custom templates, and deployment to GitHub Pages.
Learning Hub Characteristics:
- Size: 500+ markdown files
- Sections: News, Events, Tech, How-to, Issues, Ideas, Travel
- Current tech: Quarto β
docs/β GitHub Pages - Output target: Same (
docs/for GitHub Pages compatibility) - Special features: Dual YAML metadata, validation tracking, article series
π― Project Goals
What weβre building:
- Hugo site structure matching Learning Hub organization
- Custom theme similar to current Quarto cerulean theme
- Automated navigation from directory structure
- Series support for article sequences
- Search functionality with JSON index
- GitHub Pages deployment compatible with current setup
Constraints:
- β Keep existing content structure (01.00-news/, 02.00-events/, etc.)
- β Support dual YAML metadata (top for rendering, bottom for validation)
- β Maintain GitHub Pages deployment workflow
- β Fast builds (<10 seconds for 500 files)
π¦ Installation and Setup
Step 1: Install Hugo Extended
Windows (PowerShell):
# Using Chocolatey
choco install hugo-extended
# Or using Scoop
scoop install hugo-extended
# Verify installation
hugo version
# Should show: hugo v0.122.0+extendedmacOS:
brew install hugo
hugo versionLinux:
wget https://github.com/gohugoio/hugo/releases/download/v0.122.0/hugo_extended_0.122.0_Linux-64bit.tar.gz
tar -xzf hugo_extended_0.122.0_Linux-64bit.tar.gz
sudo mv hugo /usr/local/bin/
hugo versionStep 2: Create Hugo Site Structure
Parallel to current Quarto setup:
# Navigate to Learn repository
cd c:\dev\darioa.live\darioairoldi\Learn
# Create Hugo subdirectory (option 1: side-by-side testing)
mkdir hugo-site
cd hugo-site
hugo new site . --force
# OR: Initialize Hugo in root (option 2: migration path)
# hugo new site . --forceResulting structure:
Learn/
βββ _quarto.yml # Existing Quarto config
βββ 01.00-news/ # Existing content
βββ 02.00-events/ # Existing content
βββ ...
βββ hugo-site/ # New Hugo site
β βββ config.toml # Hugo configuration
β βββ content/ # Hugo content (symlink or copy)
β βββ layouts/ # Custom templates
β βββ static/ # Static assets
β βββ themes/ # Theme directory
βββ docs/ # Output (current Quarto, future Hugo)
βοΈ Hugo Configuration
config.toml
hugo-site/config.toml:
baseURL = "https://darioairoldi.github.io/Learn/"
languageCode = "en-us"
title = "Dario's Learning Hub"
theme = "learnhub-theme"
# Output to docs/ for GitHub Pages compatibility
publishDir = "../docs"
# Preserve section structure
[permalinks]
news = "/news/:slug/"
events = "/events/:slug/"
tech = "/tech/:sections[1:]/:slug/"
howto = "/howto/:slug/"
issues = "/issues/:slug/"
ideas = "/ideas/:slug/"
# Taxonomies for Learning Hub
[taxonomies]
tag = "tags"
category = "categories"
series = "series"
technology = "technologies"
event = "events"
# Markdown configuration
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true # Allow HTML (for dual YAML metadata)
[markup.goldmark.parser]
autoHeadingID = true
autoHeadingIDType = "github"
[markup.goldmark.parser.attribute]
block = true
title = true
[markup.goldmark.extensions]
definitionList = true
footnote = true
linkify = true
strikethrough = true
table = true
taskList = true
typographer = true
[markup.highlight]
anchorLineNos = true
codeFences = true
guessSyntax = false
lineNumbersInTable = true
noClasses = false
style = "github"
tabWidth = 2
[markup.tableOfContents]
endLevel = 3
ordered = false
startLevel = 2
# Site parameters
[params]
description = "Technical learning notes and documentation"
author = "Dario Airoldi"
dateFormat = "January 2, 2006"
# Enable features
toc = true
readingTime = true
searchEnabled = true
# Social/contact
github = "https://github.com/darioairoldi"
[outputs]
home = ["HTML", "RSS", "JSON"] # JSON for search
section = ["HTML", "RSS"]
page = ["HTML"]
# Menu configuration (auto-generated from content)
[menu]
[[menu.main]]
name = "News"
identifier = "news"
url = "/news/"
weight = 1
[[menu.main]]
name = "Events"
identifier = "events"
url = "/events/"
weight = 2
[[menu.main]]
name = "Tech"
identifier = "tech"
url = "/tech/"
weight = 3
[[menu.main]]
name = "How-to"
identifier = "howto"
url = "/howto/"
weight = 4
[[menu.main]]
name = "Issues"
identifier = "issues"
url = "/issues/"
weight = 5
[[menu.main]]
name = "Ideas"
identifier = "ideas"
url = "/ideas/"
weight = 6
# Build configuration
[build]
writeStats = true
[[build.cachebusters]]
source = "assets/.*\\.(js|ts|jsx|tsx)"
target = "(js|scripts|javascript)"
[[build.cachebusters]]
source = "assets/.*\\.(.*)$"
target = "$1"π Content Organization
Mapping Quarto to Hugo Structure
Current Quarto:
01.00-news/20251224-vscode-v1.107-release/
βββ 01-summary.md
βββ 02-readme.sonnet4.md
Hugo equivalent (option 1 - page bundle):
content/news/20251224-vscode-v1107-release/
βββ index.md # Main article
βββ summary.md # Additional content
βββ images/ # Co-located images
Hugo equivalent (option 2 - section with files):
content/news/
βββ 20251224-vscode-v1107-release.md
βββ 20251224-vscode-v1107-release-summary.md
Content Directory Setup
hugo-site/content/ structure:
# Create section directories
mkdir -p content/{news,events,tech,howto,issues,ideas,travel}
# Create section homepages (_index.md)
# These define section landing pages**content/news/_index.md:**
---
title: "Latest News"
description: "Updates on VS Code, tools, and technologies"
menu:
main:
name: "News"
weight: 1
---
Stay updated with the latest releases and announcements.**content/tech/_index.md:**
---
title: "Technical Articles"
description: "Deep dives into Azure, .NET, GitHub, and more"
cascade:
params:
show_toc: true
content_type: "technical"
menu:
main:
name: "Tech"
weight: 3
---
Technical documentation and learning notes organized by technology.Handling Dual YAML Metadata
Learning Hub uses dual YAML blocks (top for rendering, bottom for validation). Hugoβs front matter parser handles only the top YAML.
Current format:
---
title: "Article Title"
author: "Dario Airoldi"
date: "2026-01-14"
categories: [hugo, documentation]
---
Article content...
<!-- Validation Metadata (Do Not Edit) -->
<!--
article_metadata:
filename: "article.md"
series: "Hugo Guide"
position: 5
validation_status:
last_run: "2026-01-14T00:00:00Z"
status: "validated"
-->Hugo behavior:
- β Top YAML parsed as front matter
- β HTML comment ignored (not rendered)
- β
Goldmark with
unsafe = truepreserves HTML comments - β No modification needed!
π¨ Theme Development
Creating Custom Theme
# Create theme structure
cd hugo-site
hugo new theme learnhub-themeResult:
themes/learnhub-theme/
βββ layouts/
β βββ _default/
β β βββ baseof.html # Base template
β β βββ list.html # Section pages
β β βββ single.html # Article pages
β βββ partials/
β β βββ head.html
β β βββ header.html
β β βββ footer.html
β β βββ toc.html
β βββ index.html # Homepage
βββ static/
β βββ css/
β βββ js/
βββ theme.toml
Base Template (baseof.html)
**themes/learnhub-theme/layouts/_default/baseof.html:**
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
{{ partial "head.html" . }}
<body>
{{ partial "header.html" . }}
<main id="main-content">
<div class="container">
{{ block "main" . }}{{ end }}
</div>
</main>
{{ partial "footer.html" . }}
</body>
</html>Article Template (single.html)
**themes/learnhub-theme/layouts/_default/single.html:**
{{ define "main" }}
<article class="article">
<header class="article-header">
<h1>{{ .Title }}</h1>
<div class="article-meta">
{{ with .Params.author }}
<span class="author">By {{ . }}</span>
{{ end }}
<span class="date">{{ .Date.Format "January 2, 2006" }}</span>
{{ with .Params.reading_time }}
<span class="reading-time">{{ . }} min read</span>
{{ else }}
<span class="reading-time">{{ .ReadingTime }} min read</span>
{{ end }}
</div>
{{ with .Params.categories }}
<div class="categories">
{{ range . }}
<span class="category">{{ . }}</span>
{{ end }}
</div>
{{ end }}
</header>
{{ if .Params.toc }}
<aside class="toc">
<h2>Table of Contents</h2>
{{ .TableOfContents }}
</aside>
{{ end }}
<div class="article-content">
{{ .Content }}
</div>
{{ if .Params.series }}
{{ partial "series-navigation.html" . }}
{{ end }}
{{ with .Site.RegularPages.Related . | first 5 }}
<aside class="related-articles">
<h3>Related Articles</h3>
<ul>
{{ range . }}
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</aside>
{{ end }}
<footer class="article-footer">
{{ with .Params.tags }}
<div class="tags">
<strong>Tags:</strong>
{{ range . }}
<a href="{{ "tags/" | relLangURL }}{{ . | urlize }}" class="tag">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</footer>
</article>
{{ end }}Styling (Cerulean Theme Adaptation)
themes/learnhub-theme/assets/scss/main.scss:
// Import existing cerulean.scss from Quarto setup
// Or create Hugo-compatible version
// Variables (matching Quarto cerulean theme)
$primary-color: #2FA4E7;
$text-color: #333;
$bg-color: #fff;
$code-bg: #f5f5f5;
// Base styles
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: $text-color;
background: $bg-color;
}
// Article styles
.article {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
&-header {
margin-bottom: 2rem;
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: $primary-color;
}
}
&-meta {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
span {
margin-right: 1rem;
}
}
&-content {
font-size: 1.1rem;
h2, h3, h4 {
margin-top: 2rem;
margin-bottom: 1rem;
}
code {
background: $code-bg;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
pre {
background: $code-bg;
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
}
}
// TOC styles
.toc {
background: #f9f9f9;
border-left: 4px solid $primary-color;
padding: 1rem;
margin-bottom: 2rem;
h2 {
margin-top: 0;
font-size: 1.2rem;
}
ul {
list-style: none;
padding-left: 0;
ul {
padding-left: 1.5rem;
}
}
}
// Series navigation
.series-navigation {
background: #f0f8ff;
border: 1px solid #b0d4f1;
border-radius: 5px;
padding: 1.5rem;
margin: 2rem 0;
h3 {
margin-top: 0;
color: $primary-color;
}
&-links {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
a {
flex: 1;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 3px;
text-decoration: none;
color: $primary-color;
&:hover {
background: #f5f5f5;
}
&.series-next {
text-align: right;
margin-left: 1rem;
}
&.series-prev {
margin-right: 1rem;
}
}
}
.series-list {
ol {
padding-left: 1.5rem;
li {
margin: 0.5rem 0;
&.current {
font-weight: bold;
color: $primary-color;
}
}
}
}
}π Search Implementation
JSON Index Generation
layouts/index.json:
{{- $pages := .Site.RegularPages -}}
[
{{- range $index, $page := $pages -}}
{{- if $index }},{{ end }}
{
"title": {{ $page.Title | jsonify }},
"url": {{ $page.Permalink | jsonify }},
"content": {{ $page.Plain | jsonify }},
"summary": {{ $page.Summary | jsonify }},
"date": {{ $page.Date.Format "2006-01-02" | jsonify }},
"categories": {{ $page.Params.categories | jsonify }},
"tags": {{ $page.Params.tags | jsonify }},
"section": {{ $page.Section | jsonify }}
}
{{- end -}}
]Client-side search with Lunr.js:
themes/learnhub-theme/layouts/partials/search.html:
<div id="search-container">
<input type="text" id="search-input" placeholder="Search articles...">
<div id="search-results"></div>
</div>
<script src="https://unpkg.com/lunr/lunr.js"></script>
<script>
fetch('/index.json')
.then(response => response.json())
.then(data => {
const idx = lunr(function () {
this.ref('url')
this.field('title', { boost: 10 })
this.field('content')
this.field('tags', { boost: 5 })
this.field('categories', { boost: 5 })
data.forEach(doc => {
this.add(doc)
})
})
const searchInput = document.getElementById('search-input')
const searchResults = document.getElementById('search-results')
searchInput.addEventListener('input', function(e) {
const query = e.target.value
if (query.length < 2) {
searchResults.innerHTML = ''
return
}
const results = idx.search(query)
const matches = results.map(result => {
return data.find(page => page.url === result.ref)
})
searchResults.innerHTML = matches.map(match => `
<div class="search-result">
<a href="${match.url}">
<strong>${match.title}</strong>
<p>${match.summary}</p>
<small>${match.section} | ${match.date}</small>
</a>
</div>
`).join('')
})
})
</script>π Deployment to GitHub Pages
GitHub Actions Workflow
.github/workflows/hugo.yml:
name: Deploy Hugo Site to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.122.0'
extended: true
- name: Build Hugo Site
run: |
cd hugo-site
hugo --minify --gc
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./docs
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4Local Development Workflow
# Navigate to Hugo site
cd hugo-site
# Start development server
hugo server -D
# Visit: http://localhost:1313
# Changes auto-reload instantly (10-100ms)
# Build for production
hugo --minify --gc
# Output in docs/ directory
# Ready for GitHub Pagesπ‘ Content Migration Strategy
Step-by-Step Migration
Phase 1: Setup (Week 1)
- β Install Hugo extended
- β Create hugo-site directory
- β Configure config.toml
- β Create custom theme structure
- β Test with sample content
Phase 2: Template Development (Week 2)
- β Build baseof.html (base template)
- β Create single.html (article template)
- β Develop partials (header, footer, TOC)
- β Implement series navigation
- β Port cerulean.scss styling
Phase 3: Content Migration (Week 3-4)
- Copy one section (e.g., β03.00-tech/20.01-markdown/β)
- Update front matter if needed
- Test rendering
- Fix any issues
- Repeat for other sections
Phase 4: Features (Week 5)
- Implement search (JSON + Lunr.js)
- Add related articles
- Configure taxonomies
- Optimize images
Phase 5: Deployment (Week 6)
- Update GitHub Actions
- Test deployment
- Verify GitHub Pages
- Performance testing
Automated Content Copy Script
scripts/migrate-to-hugo.ps1:
# Migrate content from Quarto to Hugo structure
$sourceRoot = "."
$hugoContent = "hugo-site\content"
# Define section mappings
$sections = @{
"01.00-news" = "news"
"02.00-events" = "events"
"03.00-tech" = "tech"
"04.00-howto" = "howto"
"05.00-issues" = "issues"
"06.00-idea" = "ideas"
"90.00-travel" = "travel"
}
foreach ($source in $sections.Keys) {
$dest = $sections[$source]
$sourcePath = Join-Path $sourceRoot $source
$destPath = Join-Path $hugoContent $dest
Write-Host "Copying $source -> $dest"
if (Test-Path $sourcePath) {
# Create destination
New-Item -ItemType Directory -Force -Path $destPath | Out-Null
# Copy markdown files
Get-ChildItem -Path $sourcePath -Recurse -Include *.md | ForEach-Object {
$relativePath = $_.FullName.Substring($sourcePath.Length + 1)
$targetPath = Join-Path $destPath $relativePath
$targetDir = Split-Path $targetPath -Parent
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
Copy-Item $_.FullName $targetPath -Force
Write-Host " Copied: $relativePath"
}
# Copy images
Get-ChildItem -Path $sourcePath -Recurse -Include *.png,*.jpg,*.jpeg,*.gif,*.svg | ForEach-Object {
$relativePath = $_.FullName.Substring($sourcePath.Length + 1)
$targetPath = Join-Path $destPath $relativePath
$targetDir = Split-Path $targetPath -Parent
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
Copy-Item $_.FullName $targetPath -Force
}
}
}
Write-Host "Migration complete!"π― Key Takeaways
- Hugo can render Learning Hub with similar structure to Quarto
- Build time improvement: 90-180s β 5-10s (18x faster)
- Dual YAML metadata preserved - HTML comments ignored by Hugo
- Series navigation implemented with custom partials
- Search functionality via JSON index + Lunr.js
- GitHub Pages compatible - Output to
docs/directory
π Next Steps
Explore more advanced topics (coming soon in this series):
- Performance Optimization - Maximize build speed
- Theme Customization - Advanced theming
- Deployment Options - Various hosting platforms
π References
- Hugo Directory Structure π Official - Project organization
- Hugo Themes Guide π Official - Theme development
- Lunr.js π Official - Client-side search library
- GitHub Actions for Hugo π Official - CI/CD setup
- Learning Hub Repository π Official - Current implementation