A small theme for HUGO static site generator, focused on semantic HTML and accessibility and readability.
inside your HUGO project folder
git submodule add https://github.com/thomasuebel/hugo-theme-sm0l.git themes/sm0l
add
theme = 'sm0l'
Built with help from Eric Murphy's HUGO Starter Theme. To learn more about building themes in Hugo, refer to Hugo's templating documentation.
The theme is built around the idea that the IndieWeb needs all the support it can get. The Author information that you put in your configuration is rendered as an IndieWeb h-card. Into your sites index file.
baseURL = 'https://yourwebsite.com/'
title = "That's good enough" # blog name — shown in nav bar and as subtitle on homepage
theme = 'sm0l'
enableRobotsTXT = true
[params.author]
name = "your name" # used in homepage h1 ("{name}'s Blog"), post bylines, and <title> suffix
avatar = "/images/avatar.png"
bio = "Short bio or description" # optional, visible on homepage (see below)
email = "you@example.com" # optional, hidden u-email in h-card (machine-readable only)
cv = "/files/cv.pdf" # optional, emits <link rel="cv"> in <head>
By default, the homepage <title> uses site.Title and inner pages show their page title followed by the author name (e.g. "My Post — Thomas Uebel"). You can override the homepage title for search engines without affecting the nav bar or h1:
[params]
seoTitle = "Thomas Uebel — Software Engineer & Engineering Leader (Berlin)"When seoTitle is not set, the homepage falls back to site.Title. Inner pages always render as Page Title — Author Name (falling back to site.Title if no author name is configured).
The author name in blog post metadata links to /about/ with rel="author". This creates an internal link signal from every post to the about page, helping search engines associate content with the author entity. No configuration needed — it works automatically when params.author.name is set.
When a post's .Lastmod differs from .Date, the last-updated date is shown inline next to the publish date in the post metadata, e.g. "May 1, 2026 (last updated: May 10, 2026)". The date is rendered as a <time class="dt-updated"> element for Microformats compatibility.
To populate .Lastmod, either enable git-based dates in your hugo.toml:
enableGitInfo = trueOr set lastmod manually in post front matter:
lastmod = 2026-05-10The author bio (params.author.bio) is displayed visibly on the homepage below the author name. This gives search engines above-the-fold disambiguating text about the site owner. To hide the bio while keeping it accessible to screen readers:
[params]
showBio = falseWhen showBio is false, the bio is still present in the HTML (for h-card/microformats parsers) but visually hidden. The default is true (visible).
The homepage <h1> defaults to "{author.name}'s Blog" (e.g. "Thomas Uebel's Blog"). Below it, site.Title is displayed as the blog name subtitle. The nav bar also shows site.Title.
To override the H1:
[params]
homepageHeading = "Custom Heading Here"The <h1> carries the p-name microformat class (part of the h-card), since it identifies the author. The blog name subtitle has no microformat semantics.
The theme outputs Schema.org JSON-LD structured data when [params.schema] is configured. If not set, no JSON-LD is emitted (graceful no-op).
[params.schema]
givenName = "Thomas"
familyName = "Uebel"
jobTitle = "Software Engineer"
description = "Engineering leader based in Berlin"
addressLocality = "Berlin"
addressCountry = "DE"
knowsAbout = ["Go", "Hugo", "Web Performance"]
sameAs = ["https://github.com/thomasuebel", "https://linkedin.com/in/thomasuebel"]
[[params.schema.alumniOf]]
name = "University Name"
url = "https://example.edu"Each page emits a single <script type="application/ld+json"> block containing an @graph with:
| Page | Entities |
|---|---|
| Homepage | Person + WebSite |
/about/ |
Person + AboutPage |
Blog list (/blog/) |
Person + CollectionPage |
| Blog post | Person + BlogPosting (with dates, headline, keywords) |
The Person entity uses @id: /#person so all other entities reference it without duplication.
Place your avatar image PNG into your assets/images/ directory as avatar.png. It will override the themes avatar image.
To enable IntenseDebate commenting system, add your IntenseDebate account ID to your hugo.toml:
[params.intensedebate]
acct = "your_intensedebate_account_id"
Once enabled, comment counts will appear on:
- Post listing pages (list.html) next to the publication date
- Individual post pages (post-metadata.html) in the metadata section
- Full comment threads will appear at the bottom of individual posts
To get your IntenseDebate account ID, sign up at https://intensedebate.com and find your account ID in your dashboard.
Webmention is a W3C standard that enables cross-site conversations on the indie web. When someone replies to, likes, or reposts one of your blog posts from their own site, a webmention notifies you so you can display that interaction.
Static sites can't receive POST requests directly, so the theme integrates with webmention.io, a free hosted service that collects webmentions on your behalf.
-
Sign in at webmention.io with your domain. Your site needs a
rel="me"link pointing to a verified profile (the theme already addsrel="me"to social links configured inparams.social). -
Add the webmention config to your
hugo.toml:
[params.webmention]
domain = "yourdomain.com"
token = "your_webmention_io_api_token"
intro = true # optional, default true — shows an IndieWeb intro when a post has no webmentions yetYou can find your API token on your webmention.io settings page after signing in.
This does three things:
- Adds
<link rel="webmention">and<link rel="pingback">discovery tags to your<head>so other sites (and legacy Pingback clients) can find your endpoints. - Loads a small script on each post page that fetches and displays received webmentions from the webmention.io API.
- Uses your API token to authenticate requests to the webmention.io API.
Webmentions are shown at the bottom of individual blog posts, below the article content. They are grouped into two sections:
- Reactions (likes, reposts, bookmarks) appear as a row of author avatars (facepile) with a count label.
- Responses (replies, mentions) appear as a comment list with author name, avatar, date, a text preview, and a link to the original source.
Webmention and IntenseDebate are independent. You can enable one, the other, or both. When both are configured, IntenseDebate comments appear first, followed by webmentions.
Blog posts are marked up with Microformats2 h-entry properties:
| Property | Where | Description |
|---|---|---|
h-entry |
<article> |
Marks the post as an h-entry |
p-name |
post title | Name of the post |
e-content |
post body | Full content of the post |
dt-published |
publish date | ISO8601 datetime with timezone |
u-url |
hidden link | Canonical URL of the post |
p-author h-card |
post metadata | Nested author card |
p-category |
tag links | Post tags |
p-summary |
summary | Post summary if present |
u-in-reply-to |
hidden link | URL this post replies to (see below) |
This means when you link to someone else's post, their site can parse rich author and content information from yours — making your outbound webmentions more useful across the indie web.
To mark a post as a reply to another URL, add in_reply_to to the post's front matter. This renders as a hidden <a class="u-in-reply-to" rel="in-reply-to"> inside the h-entry, readable by webmention and microformat parsers but not shown to readers.
Single reply:
+++
title = "Thoughts on your post"
date = 2026-03-25T14:00:00+01:00
in_reply_to = "https://example.com/some-post"
+++
My response here...Reply to multiple posts:
+++
title = "Responding to both of you"
date = 2026-03-25T14:00:00+01:00
in_reply_to = [
"https://alice.example/post-one",
"https://bob.example/post-two"
]
+++
My response here...When webmention is configured, sending a webmention to the target URL(s) will notify those sites that you have replied.
The theme has built-in support for click event tracking with Umami or Plausible as analytics providers. The analytics script itself must already be loaded by your site (e.g. in layouts/partials/custom-head.html). This feature only fires events into the provider that is already present on the page.
Add the following to your hugo.toml. All events are opt-in — only enabled events will be tracked.
[params.analytics]
provider = "umami" # or "plausible" or "" (disabled)
[params.analytics.events]
headlineClicks = true # post title links on list pages
tagClicks = true # tag links (e.g. #hugo, #indieweb)
navClicks = true # navigation menu links
socialClicks = true # social/profile links
paginationClicks = true # pagination prev/next/page number links
replyToClicks = true # "in reply to" links on posts
commentClicks = true # comment count links
rssClicks = true # RSS feed link in footerSet provider to your analytics tool. Omit or set to "" to disable all tracking. Each event flag can be independently set to true or false (or omitted to disable).
| Event | Event Name | Properties |
|---|---|---|
| Headline clicks | headline-click |
title, url |
| Tag clicks | tag-click |
tag, url |
| Navigation clicks | nav-click |
label, url |
| Social clicks | social-click |
name, url |
| Pagination clicks | pagination-click |
label, url |
| Reply-to clicks | reply-to-click |
host, url |
| Comment clicks | comment-click |
url |
| RSS clicks | rss-click |
url |
This feature does not set cookies, load additional scripts, or collect personal data. It sends event data to an analytics provider (Umami or Plausible) that you have already loaded on your site. Both providers are designed to be cookieless and are generally considered consent-free under the EU ePrivacy Directive. However, site owners should disclose the use of analytics and event tracking in their privacy policy.
The theme's CSS is fingerprinted (content hash in the filename), which means the file can be cached indefinitely — the URL changes automatically whenever the CSS changes. To take advantage of this, configure your Apache server to set long cache headers for static assets.
Add the following to your site's .htaccess or vhost config:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|webp|png|jpg|jpeg|woff2)$">
Header set Cache-Control "max-age=31536000, immutable"
</FilesMatch>
</IfModule>This is safe because Hugo will never serve a cached stale CSS file — a changed stylesheet always gets a new URL.