Blog

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

    graficon stuff NdpksRKVEnQ unsplash scaled

    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.

  • How to Get Started with the IndieWeb Webring on Hugo

    erry s nugroho HMhG9RgADN8 unsplash scaled

    Joining the IndieWeb Webring is a great way to connect your personal Hugo site to the broader IndieWeb community. Here’s how to get started:

    1. Visit the IndieWeb Webring

    2. Set Up Authentication: IndieAuth or RelMeAuth

    • The Webring site uses IndieAuth and/or RelMeAuth for sign-in. Since Hugo sites are static, you’ll generally use RelMeAuth.

    What is RelMeAuth?

    • RelMeAuth is an authentication method based on linking your personal website to established profiles (like GitHub or Mastodon) using rel=”me” links.
    • This lets you use your social profiles to authenticate as “you”, using your site’s identity.

    Steps for Setting Up RelMeAuth

    Add rel=”me” Links

    In your Hugo site’s layouts (usually in the head partial template), add links to your existing verified social profiles with rel=”me”. For example:

    <a href="https://github.com/yourusername" rel="me">GitHub</a> <a href="https://mastodon.social/@yourusername" rel="me">Mastodon</a>

    Make sure your social profiles link back to your website as well (for bidirectional verification).

    Sign In on the Webring Site

    Click the “Sign in” button at https://xn--sr8hvo.ws/.

    Enter your personal website URL. The service will look for your social rel=”me” links and use those to authenticate you through RelMeAuth.

    Complete the authentication process by authorizing through your social profile (usually GitHub or Mastodon).

    3. Add Webring Navigation Links to Hugo

    Once authenticated and added to the webring, you’ll be instructed (or allowed) to add navigation links to your website.

    Edit your Hugo footer partial ( commonly layouts/partials/footer.html ).

    Add the following HTML snippet for the navigation links:

    <a href="https://xn--sr8hvo.ws/previous">←</a> An <a href="https://xn--sr8hvo.ws">IndieWeb Webring</a> 🕸💍 <a href="https://xn--sr8hvo.ws/next">→</a>

    This enables seamless navigation to the previous and next sites in the ring.

    4. Double-check Your Site Links (Recommended)

    • Validate that your rel=”me” links are correctly formatted. You can use indielogin.com/setup or indiewebify.me to check for errors.
    • Ensure your social profiles link back to your website (bidirectional linking is required for verification).
    • If you use domain aliases or non-www/www inconsistencies, ensure your social profiles match your canonical website URL.

    5. All Set. You’re in the Webring!

    • After your site is approved, the webring links will automatically display the correct previous/next sites based on updates at xn--sr8hvo.ws.
    • From now on, when someone visits your site, they can use the webring links to discover other IndieWeb sites.

    Optional Enhancements

    • Consider customizing the placement or style of the webring links to match your site’s theme.
    • Document any IndieWeb or Webmention features you support—many IndieWebbers like sharing how their sites interact with others.

    That’s it.
    You’re now part of a decentralized community-powered webring, using web standards and your own web identity.

  • A Practical Guide to Properly Theming Hugo Glossary Sections (and Avoiding Template Duplication Hell)

    puzzle creative KL689xrb ds unsplash scaled

    If you want your Hugo-powered glossary (用語集) section to have its own navigation bar or layout while keeping your codebase lean, read on! Here’s what really matters, and the “aha!” moments you need to know.


    Step 1: Creating a Glossary Section

    Just put your glossary Markdown files under content/en/glossary/ and content/ja/glossary/.
    You don’t need anything extra for Hugo to treat them as a unique section.

    Directory example:

    content/
      en/
        glossary/
          apple.md
          bitcoin.md
      ja/
        glossary/
          apple.md
          bitcoin.md

    Step 2: Making a Separate Glossary Navigation Partial

    Want your glossary pages to have a custom menu? Just create:

    layouts/partials/glossary/nav.html

    Put your glossary-specific navigation HTML here.
    This can be a glossary term list, an index, or whatever special menu you want.


    Step 3: Using a Custom baseof.html for the Glossary

    Here’s a Hugo superpower: you can use a “base template” just for the glossary section (with its own nav, etc.) by making:

    layouts/glossary/baseof.html

    Example:

    <!DOCTYPE html>
    <html lang="{{ .Lang }}">
      <head>
        {{ partial "head.html" . }}
      </head>
      <body>
        {{ partial "glossary/nav.html" . }}
        {{ block "main" . }}{{ end }}
        {{ partial "footer.html" . }}
      </body>
    </html>
    

    This ensures every glossary page uses your specific glossary nav and layout!


    Step 4: No Need to Duplicate single.html

    At first, you might think you need to copy single.html into layouts/glossary/single.html to get the layout working.
    But you often don’t!
    As long as your glossary content files have the correct front matter, Hugo will use your layouts/glossary/baseof.html.

    Pro-tip: The main thing Hugo cares about is the “type” or the folder

    • If you leave type unset, Hugo uses the section (here, “glossary”) as the type.
    • So, all .md files under content/en/glossary with either:
      • No type at all
      • Or type = “glossary”
        will automatically use layouts/glossary/baseof.html.

    The Real Key: Check Your .md File Front Matter

    If your glossary .md files have

    type = "post"

    Hugo will never use your glossary layouts, even if you made layouts/glossary/baseof.html!

    Solution:
    For each .md file in your glossary folders:

    • Remove type (recommended for most)
    • Or set type = “glossary”

    That’s it! Once you do this, Hugo will apply your section-specific layouts and partials—no need to duplicate single.html or fight with conditionals.


    Example Directory Structure

    layouts/
      glossary/
        baseof.html         # Glossary exclusive base layout!
      partials/
        nav.html            # General nav
        glossary/
          nav.html          # Glossary-exclusive nav bar!
      _default/
        baseof.html         # Fallback for other sections
    

    Summary

    • Just putting Markdown files under content/en/glossary makes Hugo treat them separately—no extra config needed.
    • Create layouts/partials/glossary/nav.html and layouts/glossary/baseof.html for custom nav/layout.
    • No need to duplicate single.html!
    • Always check the front matter:
      Ensure glossary items do not have type = “post”. Use no type or type = “glossary”.

    Remember:
    Controlling Hugo’s template application is about content organization and front matter—not copying template files or writing complex conditionals!


    This workflow will keep your Hugo theme simple, DRY, and powerful.
    No more struggling with broken layouts or unnecessary template duplication for your custom sections. Happy Hugo theming! 🎉