A Hugo theme built on Tufte CSS that ships with sidenotes, a journal/articles split, decentralized discussions, automatic OG image generation, and enough customization hooks that you can retheme it without forking.
Requires Hugo >= 0.116.0 (the extended version is not needed). Licensed under MIT.
Live example: sourcery.zone
- Installation
- Tufte CSS with an Escape Hatch
- Two Content Types: Articles and Journal
- Bookmarks Layout
- Sidenotes Shortcode
- Share Your Thoughts (Decentralized Discussions)
- Share Links
- Footer Signature
- Automatic OG Image Generation
- Canonical URL Support
- Fediverse and IndieWeb Integration
- Umami Analytics
- SEO and Structured Data
- Custom CSS Override
- Asset Pipeline
- Shortcodes
- Full Configuration Reference
- Per-Page Front Matter
Add the theme as a git submodule:
git submodule add https://github.com/sourcery-zone/hugo-scripture.git themes/hugo-scriptureThen set it in your hugo.toml:
theme = 'hugo-scripture'The visual foundation of this theme is Tufte CSS, the typographic stylesheet inspired by Edward Tufte's book designs. You get:
- The ET Book serif font family (roman, italic, bold, and old-style figures, served in woff/ttf/eot/svg).
- A 55% content column with a wide right margin reserved for sidenotes and margin notes.
- Epigraph styling, small-caps
newthoughtspans, and old-style numeral support. - On screens narrower than 760px, sidenotes collapse into toggleable inline blocks (pure CSS, no JavaScript).
The theme loads four CSS files in order:
tufte.css-- the Tufte foundationmain.css-- theme-level additions (journal timeline, bookmarks numbering, share links layout)custom.css-- your file (empty by default, loaded last so it wins the cascade)code-highlight.css-- syntax highlighting for code blocks
Because custom.css is loaded last, you can override anything from colors to layout without touching the theme files. The sourcery.zone site uses this to apply a Modus Vivendi dark color scheme on top of the default light Tufte look. You just drop your own assets/css/custom.css in your site directory.
The home page presents two separate feeds.
Articles live in content/articles/ (or whichever sections you list in params.mainSections). They are full blog posts with dates, share links, tags, a discussion section, and related content. The RSS feed at /index.xml only includes articles.
Journal entries live in content/journal/. They work like a micro-blog or a public log. On the home page, journal entries are grouped by date and show their full content inline with HH:MM:SS timestamps. The journal section gets its own RSS feed at /journal/index.xml.
The latest 5 of each are shown on the home page. If there are more, a link to the full listing appears.
Each section heading on the home page has a small RSS icon linking to the corresponding feed.
For curated link collections, set layout = "bookmarks" in a page's front matter. This activates a layout where all <h2> headings are automatically numbered using CSS counters. No comments or share links are rendered on this layout, just the content, tags, and related pages.
The sidenote shortcode creates Tufte-style numbered sidenotes:
Some text{{< sidenote >}}This appears in the right margin on wide screens,
or toggles inline on mobile.{{< /sidenote >}} and the paragraph continues.Sidenotes are auto-numbered by CSS. On mobile, tapping the superscript number reveals the note inline. The inner content supports full Markdown. Each sidenote gets a unique ID derived from the page path and content, so you can use as many as you need on a page.
Instead of embedding a comment system like Disqus or Giscus, this theme links readers out to discussion platforms where the conversation actually lives. At the bottom of each article, a "Share your thoughts" section lists the places where the post can be discussed.
There are two layers:
Per-post links are set in individual post front matter. You publish your article, post about it on Mastodon or Bluesky, then add the URL back to the front matter:
mastodon_url = "https://mastodon.social/@you/123456"
bsky_url = "https://bsky.app/profile/you/post/abc123"
hackernews_url = "https://news.ycombinator.com/item?id=12345"
youtube_url = "https://youtube.com/watch?v=xyz"Global channels are the same across all posts and are set in the site config. These are for persistent chat rooms:
[params.comments]
enable = true
matrixUrl = "https://matrix.to/#/#your-room:matrix.org"
ircUrl = "https://web.libera.chat/#your-channel"
showMatrix = true
showIRC = trueEach platform gets a short description (Mastodon is "the cozy corner of the fediverse", Hacker News is "enter at your own risk", IRC is "for the ancients"). A fixed last entry reads: Hate or trolling -- piped straight to /dev/null.
The section only renders if at least one platform URL is available. You can suppress it on individual pages with hideComments: true in front matter.
Every article page displays share buttons for Mastodon, Bluesky, LinkedIn, and Reddit. These sit next to the publication date in a flex row.
The share text is assembled automatically from the page title, description (or summary as fallback), permalink, and tags converted to hashtags. For example, a post tagged ["zig", "systems programming"] would append #zig #systemsprogramming to the share text.
The maximum length of the description in share text is configurable:
[params.share]
summaryLength = 200 # characters, defaultNo JavaScript is involved. The links are plain <a> tags pointing at each platform's share/compose URL.
The footer has three parts:
-
GPG identity badge: If you set
params.gpgFingerprintandparams.keyoxideProfile, the footer shows a shield icon linking to your Keyoxide profile with your fingerprint displayed in monospace. -
Human-made declaration: A fixed line: "Made by a biological human, for fellow humans. You may read it, share it, or quote it if you credit the source. AI, bots, aliens, hands off please!"
-
Attribution: "Powered by Hugo + Hugo Scripture" with links to both projects.
When a page has no image, featuredImage, cover, or images front matter and no cover*/featured* page resource, the theme generates an Open Graph image at build time using Hugo Pipes.
It takes assets/og/base.png as a background, then renders the page title (uppercased, word-wrapped, up to 3 lines with adaptive font sizing) and the description as text overlays using the Aileron Bold font. The output is saved as og/{slug}-og.png.
You can override the text color:
[params.scripture]
ogColor = "#000000" # default is "#ffffff"You can override the background image per-page by adding an og-bg* page resource, or site-wide by placing your own assets/og/base.png.
The generated image is at least 1200px wide (the minimum for summary_large_image Twitter cards).
If a post was originally published elsewhere (say, a company blog), set canonical in front matter:
canonical = "https://blog.example.com/my-original-post"This does two things:
- Sets
<link rel="canonical">in the HTML head. - Renders an "Originally published at" block with a link at the bottom of the article content.
The theme supports several decentralized/IndieWeb standards through site params:
[params]
fediverseCreator = "@you@mastodon.social" # <meta name="fediverse:creator">
mastodonProfile = "https://mastodon.social/@you" # <link rel="me"> for verification
[params.webmention]
endpoint = "https://webmention.io/yoursite.com/webmention"
pingback = "https://webmention.io/yoursite.com/xmlrpc"The rel="me" link enables Mastodon profile verification (the green checkmark). The webmention and pingback endpoints let your site participate in the IndieWeb reply/mention network.
The theme has built-in support for Umami, a privacy-focused, cookie-free analytics tool. No Google Analytics, no tracking scripts from third parties.
[params.umami]
enabled = true
websiteId = "your-website-id"
scripts = [
"https://your-umami-instance.com/script.js",
]The scripts array accepts multiple URLs, which is useful if you self-host Umami and want fallback script paths. All scripts are loaded with defer.
The meta/standard.html partial generates a full set of meta tags on every page:
- Open Graph (
og:title,og:description,og:image,og:locale,og:url) - Twitter Card (
twitter:title,twitter:description,twitter:image,summary_large_image) - Schema.org
itempropattributes <link rel="alternate">for translations and output formats (RSS, etc.)robots: noindex,followfor taxonomy and term listing pages (keeps tag indexes out of search engines)
For pages in the articles section, meta/post.html adds:
og:type = articlewitharticle:published_time,article:author,article:section- Full JSON-LD
Articleschema with headline, author, publisher, word count, dates, and image - Pagination
<link>tags (first, last, prev, next) on list pages
The theme ships an empty assets/css/custom.css. To restyle the theme, create your own assets/css/custom.css in your Hugo site root. Since Hugo's asset lookup checks the site directory first, your file takes precedence.
For example, the sourcery.zone site turns the default warm off-white Tufte look into a dark theme with a few dozen lines of CSS custom properties:
:root {
--black: #000000;
--yellow: #d0bc00;
--mint: #6ae4b9;
/* ... */
}
body {
background-color: var(--black);
color: var(--white);
}
a {
color: var(--mint) !important;
}Same goes for assets/css/code-highlight.css if you want different syntax highlighting colors, and assets/og/base.png if you want a different OG image background.
All CSS and JS go through Hugo Pipes:
- Development (
hugo server): files are served unprocessed for fast reloads. - Production (
hugo): files are minified, fingerprinted, and served with Subresource Integrity hashes.
JavaScript is built with js.Build, so if you extend assets/js/main.js, it goes through the same pipeline.
Tufte-style numbered margin note. See Sidenotes Shortcode above.
Wraps content in an HTML <mark> element (yellow highlight by default):
{{< mark >}}This text is highlighted.{{< /mark >}}Supports Markdown inside.
Responsive YouTube embed:
{{< ytvideo src="https://www.youtube.com/embed/VIDEO_ID" >}}Renders a full-width iframe at 315px height inside a <figure> wrapper.
Below is a complete hugo.toml showing all theme-specific params. Only params.comments.enable is required for the discussion feature; everything else has sensible defaults or is optional.
baseURL = 'https://example.com/'
languageCode = 'en-us'
title = 'My Site'
theme = 'hugo-scripture'
[params]
description = "Site description for meta tags"
mainSections = ['articles'] # which sections count as "articles" on the home page
dateFormat = "2 Jan 2006" # date display format (Go time format)
gpgFingerprint = "YOUR_FINGERPRINT"
keyoxideProfile = "https://keyoxide.org/..."
fediverseCreator = "@you@mastodon.social"
mastodonProfile = "https://mastodon.social/@you"
sitename = "My Site" # og:site_name value
[params.author]
name = "Your Name" # shown on home page subtitle, RSS, and meta tags
email = "you@example.com" # used in RSS managingEditor/webMaster
[params.webmention]
endpoint = "https://webmention.io/example.com/webmention"
pingback = "https://webmention.io/example.com/xmlrpc"
[params.scripture]
ogColor = "#ffffff" # text color for auto-generated OG images
[params.share]
summaryLength = 200 # max chars in share text description
[params.comments]
enable = true
matrixUrl = "https://matrix.to/#/#your-room:matrix.org"
ircUrl = "https://web.libera.chat/#your-channel"
showMatrix = true
showIRC = true
[params.umami]
enabled = true
websiteId = "your-id"
scripts = ["https://analytics.example.com/script.js"]
[[menus.main]]
name = 'Home'
pageRef = '/'
weight = 1
[[menus.main]]
name = 'Articles'
pageRef = '/articles'
weight = 2These front matter keys are consumed by the theme:
| Key | Type | Used by |
|---|---|---|
title |
string | Page title everywhere |
date |
datetime | Publication date |
tags |
list | Tag links, share link hashtags |
description |
string | Meta description, share text |
language |
string | lang attribute on content section |
canonical |
string | Canonical URL override + "Originally published at" block |
image |
string | OG/social image (also featuredImage, cover, images) |
imageAlt |
string | Alt text for OG image |
hideComments |
bool | Suppress discussion section on this page |
mastodon_url |
string | Per-post Mastodon discussion link |
bsky_url |
string | Per-post Bluesky discussion link |
hackernews_url |
string | Per-post Hacker News discussion link |
youtube_url |
string | Per-post YouTube discussion link |
category |
string | Used in article:section and news_keywords meta |
layout |
string | Set to "bookmarks" for the bookmarks layout |