Ever wanted your Hugo site’s blog list to look like those beautiful Gatsby blogs—displaying 12 cards per page in a three-column grid, each card showing a featured image, tags (as links), an excerpt, author, read time, publish date, and last modified date? That’s exactly what I set out to do—here’s my practical walkthrough with code, tips, and gotchas!
Table of Contents
1. Final Layout Goals
Home page (index.html
):
- Three columns, four rows (12 cards/page)
- Each card shows:
- Featured image (from front matter)
- Linked tags
- Title
- Meta info: author / publish date / last modified / read time
- Excerpt
- “Read more” link
2. Modify index.html for Card Grid Layout
Define a .blog-grid
container for your grid, and loop over paginated blog posts.
text{{ define "main" }}
<div class="container" role="main">
{{ $pages := where .Site.RegularPages "Draft" false }}
{{ $paginator := .Paginate (sort $pages "Lastmod" "desc") 12 }}
<div class="blog-grid">
{{ range $page := $paginator.Pages }}
<article class="blog-card">
<!-- Featured image -->
{{ with resources.Get (printf "images/%s" $page.Params.image) }}
{{ with .Resize (printf "%dx%d webp" .Width .Height) }}
<img src="{{ .RelPermalink }}" alt="{{ $page.Title }}" class="u-photo blog-card-image" style="width:100%;height:170px;object-fit:cover;">
{{ end }}
{{ end }}
<!-- Tag links (see below for correct “/tags/” path!) -->
{{ if $page.Params.tags }}
<div class="blog-card-tags">
{{ range $page.Params.tags }}
<a href="{{ (print "/tags/" (. | urlize) "/") | relLangURL }}" class="tag p-category">{{ . }}</a>
{{ end }}
</div>
{{ end }}
<!-- Title -->
<h2 class="blog-card-title">
<a href="{{ $page.Permalink }}">{{ $page.Title }}</a>
</h2>
<!-- Meta info (see section 4) -->
<div class="blog-card-meta">
{{ partial "post_meta.html" $page }}
</div>
<!-- Excerpt -->
<div class="blog-card-excerpt">
{{ if $page.Params.excerpt }}
{{ $page.Params.excerpt | plainify | truncate 80 }}
{{ else }}
{{ $page.Summary | plainify | truncate 80 }}
{{ end }}
</div>
<!-- Read more -->
<a href="{{ $page.Permalink }}" class="blog-card-readmore">Read more →</a>
</article>
{{ end }}
</div>
<!-- ...pagination, profile box, etc, as needed -->
</div>
{{ end }}
3. Responsive Grid CSS & Dark Mode
Add this CSS (as a file or inline):
css.blog-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2em;
}
@media (max-width: 900px) { .blog-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .blog-grid { grid-template-columns: 1fr; } }
.blog-card {
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,.05);
border-radius: 10px;
padding: 1em;
color: #222;
display: flex;
flex-direction: column;
}
.blog-card-image img { object-fit: cover; width: 100%; height: 170px; border-radius: 8px 8px 0 0; }
.blog-card-tags a.tag { background: #f3f3f3; border-radius: 12px; font-size: 0.9em; margin-right: 0.4em; padding: 1px 0.8em; text-decoration: none; color: #555; }
.blog-card-title { font-size: 1.22em; margin: 0.2em 0 0.12em 0; }
.blog-card-excerpt { margin-bottom: auto; margin-top: 0.7em; }
.blog-card-readmore { color: #ff8300; font-weight: bold; text-decoration: none; }
/* Dark mode */
@media (prefers-color-scheme: dark), body.night-mode {
.blog-card { background: #1a1a1a; color: #f9f9f9; }
.blog-card-title a { color: #ffa134; }
.blog-card-tags a.tag { background: #292929; color: #ffa134; }
}
4. Displaying Publish Date, Last Modified, Author, and Read Time
Centralize your meta display using a Hugo partial, e.g. post_meta.html
:
text<span class="post-meta">
<!-- Publish date -->
<time class="dt-published" datetime="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">{{ .Date.Format "2006-01-02" }}</time>
{{ $lastmodstr := .Lastmod.Format "2006-01-02" }}
{{ $datestr := .Date.Format "2006-01-02" }}
{{ if ne $datestr $lastmodstr }}
| Last updated {{ $lastmodstr }}
{{ end }}
<!-- Reading time (auto, in minutes) -->
| <i class="fas fa-clock"></i> {{ .ReadingTime }} min
<!-- Author (from front matter or global config) -->
{{ if .Params.author }}
| <i class="fas fa-user"></i> <span class="p-author h-card">{{ .Params.author | safeHTML }}</span>
{{ else }}
| <i class="fas fa-user"></i> <span class="p-author h-card">{{ .Site.Params.author.name | safeHTML }}</span>
{{ end }}
</span>
And in your card:
text<div class="blog-card-meta">
{{ partial "post_meta.html" $page }}
</div>
- Per-post author? Add
author = "Your Name"
to the post’s front matter. - Site-wide author? Use
[params.author] name = "Your Name"
inconfig.toml
. - Reading time: Hugo auto-calculates
.ReadingTime
as estimated minutes.
5. Pro Tips
- Works with Bootstrap themes; ignore default
.row
/.col-
and use your own grid for full control. - Use Hugo’s
resources.Get
and.Resize
for images—no change needed from your original. - Centralize meta rendering (
post_meta.html
) and call it from both list and single templates. - For dark mode, don’t hard-code white—add CSS overrides using
prefers-color-scheme
or a body class. - Internationalize labels/dates using Hugo i18n if needed.
Example: front matter and config
content/blog/my-post.md
:
texttitle = "My Hugo Gatsby-Style Cards Example"
date = 2025-09-07T19:00:00+09:00
lastmod = 2025-09-08T09:00:00+09:00
tags = ["Japanese beat selling site", "AI music"]
author = "Genx"
excerpt = "How to build Gatsby-style card layouts in Hugo, step by step, including pitfalls and professional tweaks."
image = "sample-thumb.jpg"
config.toml
:
text[params.author]
name = "Genx"
Conclusion
By customizing your Hugo index.html
with a CSS Grid, improving card structure, using Hugo’s natural .ReadingTime
and partial-powered meta display, and being careful with tag URLs, you can perfectly mimic those modern Gatsby/Next.js blog UIs—in Hugo, your way!
These methods work for multilingual Hugo sites, are flexible with any base theme, and let you easily achieve that highly-polished card grid you see everywhere.
Leave a Reply