How to Build a Gatsby-Style 3×4 Card Grid Blog Layout in Hugo

graficon stuff NdpksRKVEnQ unsplash scaled
Genx Avatar

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!


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 &rarr;</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 }}
    &nbsp;| Last updated {{ $lastmodstr }}
  {{ end }}
  <!-- Reading time (auto, in minutes) -->
  &nbsp;|&nbsp;<i class="fas fa-clock"></i>&nbsp;{{ .ReadingTime }} min
  <!-- Author (from front matter or global config) -->
  {{ if .Params.author }}
    &nbsp;|&nbsp;<i class="fas fa-user"></i>&nbsp;<span class="p-author h-card">{{ .Params.author | safeHTML }}</span>
  {{ else }}
    &nbsp;|&nbsp;<i class="fas fa-user"></i>&nbsp;<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" in config.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.

Share This Post:

Genx

in

Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

10 + seven =

Webmention: Have you posted a response to this article? Let me know the URL: