Cara Membuat Theme Hugo dari Nol dengan Tailwind CSS
Cara Membuat Theme Hugo dari Nol dengan Tailwind CSS
Membuat custom theme Hugo dari nol dengan Tailwind CSS memberikan kebebasan penuh dalam mendesain website. Tutorial ini akan membimbing Anda membangun theme profesional dari awal hingga siap pakai.
Struktur Theme Hugo
Directory Structure
Theme Hugo mengikuti struktur folder standar:
themes/
└── my-custom-theme/
├── archetypes/
│ └── default.md
├── assets/
│ ├── css/
│ │ └── main.css
│ └── js/
│ └── main.js
├── layouts/
│ ├── _default/
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── single.html
│ ├── partials/
│ │ ├── head.html
│ │ ├── header.html
│ │ ├── footer.html
│ │ └── navigation.html
│ ├── shortcodes/
│ │ ├── alert.html
│ │ └── figure.html
│ └── index.html
├── static/
│ ├── images/
│ └── favicon.ico
├── config.yaml
├── theme.toml
└── README.md
Step 1: Inisialisasi Theme
1.1 Buat Folder Theme
# Buat folder theme
cd your-hugo-site
themes/
mkdir my-custom-theme
cd my-custom-theme
# Inisialisasi git (opsional)
git init
1.2 File theme.toml
File metadata untuk theme:
name = "My Custom Theme"
license = "MIT"
licenselink = "https://github.com/username/my-custom-theme/blob/main/LICENSE"
description = "A modern, responsive Hugo theme built with Tailwind CSS"
homepage = "https://github.com/username/my-custom-theme"
demosite = "https://demo.example.com"
tags = ["blog", "responsive", "tailwind", "minimal"]
features = ["responsive", "dark-mode", "search", "multilingual"]
min_version = "0.100.0"
[author]
name = "Your Name"
homepage = "https://yourwebsite.com"
[original]
name = ""
homepage = ""
repo = ""
1.3 Archetypes
Template untuk content baru (archetypes/default.md):
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
description: ""
image: ""
categories: []
tags: []
---
Write your content here...
Step 2: Setup Tailwind CSS
2.1 Initialize Node.js Project
npm init -y
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
2.2 Konfigurasi Tailwind
tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./layouts/**/*.html',
'./content/**/*.md',
'./content/**/*.html',
'../../content/**/*.md',
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
display: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [
require('@tailwindcss/typography'),
],
}
2.3 CSS Entry Point
assets/css/main.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply antialiased text-gray-900 bg-white dark:text-gray-100 dark:bg-gray-900;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold tracking-tight;
}
h1 {
@apply text-3xl md:text-4xl lg:text-5xl;
}
h2 {
@apply text-2xl md:text-3xl;
}
h3 {
@apply text-xl md:text-2xl;
}
a {
@apply text-primary-600 hover:text-primary-800 transition-colors;
}
}
@layer components {
.container-custom {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}
.prose-custom {
@apply prose prose-lg max-w-none
prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-gray-100
prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline
prose-img:rounded-xl prose-img:shadow-lg;
}
.btn {
@apply inline-flex items-center px-4 py-2 border border-transparent
text-sm font-medium rounded-md shadow-sm
focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply btn text-white bg-primary-600 hover:bg-primary-700
focus:ring-primary-500;
}
.btn-secondary {
@apply btn text-gray-700 bg-white border-gray-300 hover:bg-gray-50
focus:ring-primary-500;
}
}
@layer utilities {
.text-shadow {
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
}
Step 3: Layout Templates
3.1 Base Template (layouts/_default/baseof.html)
Template dasar yang akan di-extend:
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode | default "id" }}" class="scroll-smooth">
<head>
{{ partial "head.html" . }}
</head>
<body class="min-h-screen flex flex-col">
{{ partial "header.html" . }}
<main id="main-content" class="flex-grow">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
{{ partial "scripts.html" . }}
</body>
</html>
3.2 Head Partial (layouts/partials/head.html)
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
<!-- Tailwind CSS via Hugo Pipes -->
{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | resources.PostCSS }}
{{ if hugo.IsProduction }}
{{ $css = $css | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}">
{{ else }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">
{{ end }}
<!-- SEO Meta Tags -->
<meta name="description" content="{{ .Description | default .Site.Params.description }}">
<meta name="author" content="{{ .Site.Params.author }}">
<!-- Open Graph -->
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{ .Description }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
{{ with .Params.image }}<meta property="og:image" content="{{ . | absURL }}">{{ end }}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Title }}">
<meta name="twitter:description" content="{{ .Description }}">
{{ with .Params.image }}<meta name="twitter:image" content="{{ . | absURL }}">{{ end }}
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- Canonical URL -->
<link rel="canonical" href="{{ .Permalink }}">
3.3 Header Partial (layouts/partials/header.html)
<header class="bg-white dark:bg-gray-800 shadow-sm sticky top-0 z-50">
<nav class="container-custom">
<div class="flex justify-between items-center h-16">
<!-- Logo -->
<a href="{{ .Site.BaseURL }}" class="text-xl font-bold text-gray-900 dark:text-white">
{{ .Site.Title }}
</a>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
{{ range .Site.Menus.main }}
<a href="{{ .URL }}" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
{{ .Name }}
</a>
{{ end }}
<!-- Dark Mode Toggle -->
<button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"></path>
</svg>
<svg class="w-5 h-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
</div>
<!-- Mobile Menu Button -->
<button id="mobile-menu-btn" class="md:hidden p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden md:hidden pb-4">
{{ range .Site.Menus.main }}
<a href="{{ .URL }}" class="block py-2 text-gray-600 dark:text-gray-300">
{{ .Name }}
</a>
{{ end }}
</div>
</nav>
</header>
3.4 Footer Partial (layouts/partials/footer.html)
<footer class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800">
<div class="container-custom py-12">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Brand -->
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ .Site.Title }}</h3>
<p class="text-gray-600 dark:text-gray-400">{{ .Site.Params.description }}</p>
</div>
<!-- Quick Links -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">Menu</h4>
<ul class="space-y-2">
{{ range .Site.Menus.main }}
<li>
<a href="{{ .URL }}" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
{{ .Name }}
</a>
</li>
{{ end }}
</ul>
</div>
<!-- Social -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">Social</h4>
<div class="flex space-x-4">
{{ range .Site.Params.social }}
<a href="{{ .url }}" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span class="sr-only">{{ .name }}</span>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="{{ .icon }}"/>
</svg>
</a>
{{ end }}
</div>
</div>
</div>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-800">
<p class="text-center text-gray-500 dark:text-gray-400 text-sm">
© {{ now.Year }} {{ .Site.Title }}. All rights reserved.
</p>
</div>
</div>
</footer>
3.5 Scripts Partial (layouts/partials/scripts.html)
<!-- Dark Mode Toggle -->
<script>
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
// Check saved preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
}
themeToggle?.addEventListener('click', () => {
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
});
</script>
<!-- Mobile Menu -->
<script>
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
mobileMenuBtn?.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
</script>
{{ if hugo.IsProduction }}
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ .Site.Params.googleAnalytics }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ .Site.Params.googleAnalytics }}');
</script>
{{ end }}
Step 4: Content Templates
4.1 List Template (layouts/_default/list.html)
{{ define "main" }}
<div class="container-custom py-12">
<header class="mb-12">
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ .Title }}
</h1>
{{ with .Description }}
<p class="text-xl text-gray-600 dark:text-gray-400">{{ . }}</p>
{{ end }}
</header>
<!-- Posts Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{{ range .Pages }}
<article class="bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
{{ with .Params.image }}
<a href="{{ .RelPermalink }}">
<img src="{{ . | relURL }}" alt="{{ .Title }}" class="w-full h-48 object-cover">
</a>
{{ end }}
<div class="p-6">
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-3">
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
{{ if .Params.categories }}
<span>•</span>
<span>{{ index .Params.categories 0 }}</span>
{{ end }}
</div>
<h2 class="text-xl font-semibold mb-3">
<a href="{{ .RelPermalink }}" class="text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400">
{{ .Title }}
</a>
</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4 line-clamp-3">
{{ .Description | default .Summary | truncate 150 }}
</p>
<a href="{{ .RelPermalink }}" class="inline-flex items-center text-primary-600 dark:text-primary-400 hover:underline">
Baca selengkapnya
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</article>
{{ end }}
</div>
<!-- Pagination -->
{{ template "_internal/pagination.html" . }}
</div>
{{ end }}
4.2 Single Template (layouts/_default/single.html)
{{ define "main" }}
<article class="container-custom py-12 max-w-4xl">
<!-- Header -->
<header class="mb-8">
{{ range .Params.categories }}
<a href="/categories/{{ . | urlize }}" class="inline-block px-3 py-1 bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 text-sm font-medium rounded-full mb-4">
{{ . }}
</a>
{{ end }}
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{{ .Title }}
</h1>
<div class="flex items-center gap-4 text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<span class="text-sm">{{ .Site.Params.author }}</span>
</div>
<span>•</span>
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
<span>•</span>
<span class="text-sm">{{ .ReadingTime }} min read</span>
</div>
</header>
<!-- Featured Image -->
{{ with .Params.image }}
<figure class="mb-12">
<img src="{{ . | relURL }}" alt="{{ .Title }}" class="w-full h-64 md:h-96 object-cover rounded-xl">
</figure>
{{ end }}
<!-- Content -->
<div class="prose-custom">
{{ .Content }}
</div>
<!-- Tags -->
{{ with .Params.tags }}
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">Tags:</h4>
<div class="flex flex-wrap gap-2">
{{ range . }}
<a href="/tags/{{ . | urlize }}" class="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
#{{ . }}
</a>
{{ end }}
</div>
</div>
{{ end }}
<!-- Related Posts -->
{{ $related := .Site.RegularPages.Related . | first 3 }}
{{ with $related }}
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Artikel Terkait</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{{ range . }}
<a href="{{ .RelPermalink }}" class="block group">
<h4 class="font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
{{ .Title }}
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2">
{{ .Description | default .Summary | truncate 100 }}
</p>
</a>
{{ end }}
</div>
</div>
{{ end }}
</article>
{{ end }}
4.3 Homepage Template (layouts/index.html)
{{ define "main" }}
<!-- Hero Section -->
<section class="bg-gradient-to-br from-primary-50 to-white dark:from-gray-900 dark:to-gray-800 py-20">
<div class="container-custom text-center">
<h1 class="text-5xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6">
{{ .Site.Params.hero.title | default .Site.Title }}
</h1>
<p class="text-xl md:text-2xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto mb-8">
{{ .Site.Params.hero.description | default .Site.Params.description }}
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/posts" class="btn-primary text-lg px-8 py-3">
Jelajahi Artikel
</a>
<a href="/about" class="btn-secondary text-lg px-8 py-3">
Tentang Kami
</a>
</div>
</div>
</section>
<!-- Recent Posts -->
<section class="py-16">
<div class="container-custom">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-12 text-center">Artikel Terbaru</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{{ range first 6 .Site.RegularPages }}
<article class="bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
{{ with .Params.image }}
<a href="{{ .RelPermalink }}">
<img src="{{ . | relURL }}" alt="{{ .Title }}" class="w-full h-48 object-cover">
</a>
{{ end }}
<div class="p-6">
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-3">
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
{{ if .Params.categories }}
<span>•</span>
<span>{{ index .Params.categories 0 }}</span>
{{ end }}
</div>
<h3 class="text-xl font-semibold mb-3">
<a href="{{ .RelPermalink }}" class="text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400">
{{ .Title }}
</a>
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4 line-clamp-3">
{{ .Description | default .Summary | truncate 150 }}
</p>
<a href="{{ .RelPermalink }}" class="inline-flex items-center text-primary-600 dark:text-primary-400 hover:underline">
Baca selengkapnya
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</article>
{{ end }}
</div>
<div class="text-center mt-12">
<a href="/posts" class="btn-primary">
Lihat Semua Artikel
</a>
</div>
</div>
</section>
{{ end }}
Step 5: Shortcodes
5.1 Alert Shortcode (layouts/shortcodes/alert.html)
{{ $type := .Get "type" | default "info" }}
{{ $title := .Get "title" | default "" }}
{{ $colors := dict
"info" "bg-blue-50 text-blue-800 border-blue-200"
"warning" "bg-yellow-50 text-yellow-800 border-yellow-200"
"success" "bg-green-50 text-green-800 border-green-200"
"error" "bg-red-50 text-red-800 border-red-200"
}}
<div class="rounded-lg border-l-4 p-4 my-6 {{ index $colors $type }}">
{{ with $title }}
<h4 class="font-semibold mb-2">{{ . }}</h4>
{{ end }}
<div class="text-sm">
{{ .Inner }}
</div>
</div>
Penggunaan di content:
{{ < alert type="warning" title="Perhatian" > }}
Pastikan untuk backup data sebelum melanjutkan.
{{ < / alert > }}
5.2 Figure Shortcode (layouts/shortcodes/figure.html)
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" | default "" }}
{{ $caption := .Get "caption" | default "" }}
{{ $width := .Get "width" | default "100%" }}
<figure class="my-8">
<img src="{{ $src }}" alt="{{ $alt }}" class="rounded-xl shadow-lg w-full" style="max-width: {{ $width }};">
{{ with $caption }}
<figcaption class="text-center text-sm text-gray-600 dark:text-gray-400 mt-3">
{{ . }}
</figcaption>
{{ end }}
</figure>
Step 6: Configuration
6.1 Theme Configuration Example
config/_default/config.yaml di Hugo site:
baseURL: 'https://example.com'
languageCode: 'id'
title: 'My Hugo Site'
theme: 'my-custom-theme'
params:
author: 'John Doe'
description: 'A modern blog built with Hugo and Tailwind CSS'
hero:
title: 'Welcome to Our Blog'
description: 'Discover insightful articles about web development, design, and technology'
social:
- name: 'GitHub'
url: 'https://github.com/username'
icon: 'M12 0c-6.626...'
- name: 'Twitter'
url: 'https://twitter.com/username'
icon: 'M18.244 2.25h3.308...'
googleAnalytics: 'GA_MEASUREMENT_ID'
menu:
main:
- identifier: 'home'
name: 'Home'
url: '/'
weight: 10
- identifier: 'posts'
name: 'Posts'
url: '/posts/'
weight: 20
- identifier: 'about'
name: 'About'
url: '/about/'
weight: 30
Testing dan Deployment
Development Testing
# Run Hugo server dengan theme
cd /path/to/hugo-site
hugo server -D --themesDir themes
# Atau set theme di config dan jalankan langsung
hugo server -D
Build untuk Production
# Build dengan minification
hugo --gc --minify
# Cek hasil build
ls -la public/
Kesimpulan
Membuat theme Hugo dari nol dengan Tailwind CSS memberikan:
✅ Kontrol Penuh: Setiap aspek bisa dikustomisasi
✅ Ukuran Optimal: Hanya include yang dibutuhkan
✅ Modern Stack: Tailwind CSS v3+ dengan utility-first approach
✅ SEO Ready: Meta tags, schema markup, structured data
✅ Responsive: Mobile-first design dengan Tailwind breakpoints
✅ Dark Mode: Toggle dan system preference support
✅ Performant: Minimal CSS bundle, lazy loading images
Theme yang sudah dibuat dapat digunakan kembali di project Hugo lain atau dipublish ke Hugo Themes Gallery.
Artikel Terkait
Link Postingan: https://www.tirinfo.com/cara-membuat-theme-hugo-tailwind/