Blog posts
A blog post’s body is written exactly like a documentation page; the shell is what differs. A post carries a date, an author, tags and a featured image, the list is grouped by year newest first, and the section has an RSS feed. This page covers creating the blog section, a post’s front matter, featured images, list pagination and feeds.
The blog directory
A blog is a section under content/, and type: blog gives it the blog shell.
Subdirectories divide it by publisher and audience, with posts sitting flat
inside. Do not create year directories: the year grouping is generated by the
list page.
this site's content/blog/
content/
blog/
- _index.mdtype: blog + cascade
- _index.zh.md
oink/engineering notes and announcements
- _index.mdcascade: images: [/images/oink.webp]
- oink-announcement.md
- oink-announcement.zh.md
release/versioned release notes
- _index.mdcascade: images: [/images/releasenote.webp]
- 0.4.0.md
- 0.4.0.zh.md
The section root pushes the type down the whole subtree and sets the behaviour that section shares:
params.ui.blog_section (default blog) names where the blog root is. Rename
the directory and either change that parameter or use sidebar_root_for: self
as above.
Blog sections are expanded by default in the sidebar and ordered by date, newest
first; giving one post a weight pins it to the top.
A post’s front matter
Where it differs from a documentation page:
dateis required. It decides the post’s place in the list, its year group and its RSS timestamp. A date in the future is not built by default;hugo server -Fpreviews it.descriptionis rendered as a standfirst above the body, not only as a search snippet, so write it as a sentence for the reader.authoraccepts inline Markdown, so[Vonng](https://vonng.com)works. For more than one author, a portrait, or a profile page, use theauthorstaxonomy below instead; the two do not interfere, and a post keeps renderingauthorwhereverauthorsis absent.- The date display format comes from
params.time_format_blogand can be set per language (this site usesMonday, January 02, 2006in English and2006年1月2日in Chinese).
Bilingual posts are stored in pairs, keeping date, author, weight and
aliases identical across the two. Titles, descriptions and tags are
translated; commit IDs, version numbers, commands and URLs are not.
Featured image
Each row on a list page or a tag page has a thumbnail on the left, resolved in this order, first match winning:
imagesin the post’s front matter, first entry;- An image resource in the page bundle whose filename contains
featured(it is cropped to a thumbnail, and the resource’s ownbylinebecomes its caption); - An
imagesvalue inherited from an ancestor section’scascade, nearest first.
A section-wide default uses Hugo’s native cascade over the whole subtree; this
site sets one for each of its two subsections:
To drop the image on one post, write images: [] in its front matter; to drop
it for a whole subsection, put images: [] in that level’s cascade. The
site-level params.images is unaffected — it feeds the share card only and is
never rendered as a list thumbnail.
On the article itself
By default the resolved image appears on list rows and in the social card, and
the article itself shows nothing — write the hero by hand and it will disagree
with the card sooner or later. params.ui.featured_image renders it from the
same resolver instead:
| Mode | What the article shows |
|---|---|
none |
Nothing. The theme default, so a site that renders nothing today renders exactly the same bytes |
banner |
The image above the title in a fixed 16:9 figure, so a run of articles keeps one rhythm |
wash |
The image behind the article header at a tenth of its opacity, masked to nothing before the text starts — the post takes a colour from its subject without spending any contrast on it |
The page key is featured_image, so a cascade on one subsection turns it on
for that tree and a single post can opt out. A post with no image renders
nothing in either mode, which is why a section can carry the switch for a run of
posts that do not all have art. Neither mode adds a script or a bundle member.
List pages and pagination
After the body of the section _index.md, the theme appends the post list:
grouped by year (“Posted in 2026”), years newest first, each row showing the
title, date, subsection, tags, thumbnail and the first 250 characters of the
body as a summary.
Pagination uses Hugo’s native paginator, ten posts per page by default, adjusted
in hugo.yml:
The values and the remaining pagination options are in the Hugo documentation.
The card form
params.ui.blog_index: cards renders the same list as a grid of content cards
instead of rows: a 16:9 crop of the post’s image above the title, the date and
subsection line, and a three-line summary.
The choice is presentational only — year grouping, pagination and manual_link
behave identically, and the row output is unchanged to the byte. The column
count applies above the xl breakpoint; between md and xl the grid is two
columns and below md it is one. Front matter blog_index on a blog root, or its
cascade, sets it per section. Term and taxonomy pages keep the row list, and
there is no reader-side switch between the two forms.
Card images go through Hugo’s .Fill whenever the resource can be processed, so
a grid of posts does not download a full-size original per card.
RSS
Which pages produce a feed is decided by outputs. Adding RSS to section
gives every section its own feed:
Writing outputs at all replaces Hugo’s defaults wholesale, so RSS has to be
written back explicitly. Omitting it turns off the feed for that page kind, and
the build does not complain.
This site therefore has /blog/index.xml (the whole blog) and
/blog/release/index.xml (release notes only). A section feed recursively
includes every subsection’s posts, so subscribing to /blog/ covers
everything. An individual post has no .xml of its own.
Each language has its own feed at that language’s route plus index.xml. The
item limit is Hugo’s services.rss.limit. On the blog root and its first-level
subsection pages, the first action button beside the title row is the RSS link,
so a reader need not assemble the address by hand.
To drop feeds site-wide, turn the kind off with disableKinds, which is more
thorough than removing RSS from each page kind:
Components degrade to their static shape in a feed: disclosures are expanded and interactive controls are removed. The four-output rules are the same for blog posts as for documentation.
Categories and tags
tags and categories are Hugo’s taxonomies, and the theme renders them as
chips in the post header, a tag cloud in the right column, and a filter menu in
the navbar. Enabling them, bilingual term labels, and switching them per content
type are covered in Taxonomies.
Release notes
A versioned release announcement is an ordinary post, conventionally under
blog/release/, with the version in linkTitle (Oink v0.4.0). For a download
page with release cards, asset tables and checksums, see
Releases and downloads.
Components in a post
Callouts, tabs, code blocks, images and tables work exactly as on a
documentation page; the syntax is in Components. Headings
in a post body take explicit English {#id} anchors too.
The four blocks at the end of a post — feedback, last modified, pager, comments — behave as on a documentation page; see Writing pages. A blog usually turns feedback off and keeps comments.
Authors and bylines
Declaring the taxonomy is the entire switch; the theme adds no parameter:
A post then names its authors in order:
The article head renders portraits and linked names in exactly that order — the
front matter sequence is both the set and the order — a list row renders the
names, and the blog feed emits one <dc:creator> per author per item beside the
site-level managingEditor. Names are separated by a CSS gap rather than a
connector word, because “and” is a per-locale decision and there are 32 locales.
An author’s profile is simply the term page, so there is no data/authors file
to disagree with it:
The display name is the term page’s link title — linkTitle when it has
one, title otherwise — so a profile can carry a full name and byline a short
handle. description is the one-line introduction, the body the long one, and
the avatar is whatever the featured-image resolver selects for that page — so images: and a bundled portrait follow the same rules an
article’s own image follows. A bilingual profile is an _index.zh.md beside it.
A name a post uses but nobody gave a profile page still bylines: the link title,
an initial, and a link to its archive.
The 0.4 author: string is untouched wherever authors is absent, and neither
form warns about the other.
Series
A series is a reading path through articles that each stand alone. Numbering, cross-references and aggregate output belong to Book; this is the lighter thing. Declaring the taxonomy is again the whole switch:
An article names the series and may place itself in it:
It then carries a strip above its body naming the series, its position, the next
part, and the whole list behind a <details> — no JavaScript, no bundle member.
The term page content/series/<name>/_index.md is the introduction, and an
_index.zh.md beside it makes the pair bilingual.
Reading order is the theme’s own, because a term page cannot supply one: Hugo’s
taxonomy weight reaches neither Page.Weight nor GroupByParam. Weighted
members come first in ascending series_weight, the rest follow by ascending
date, and Path breaks a tie. The strip and the term page read the same
resolver, so they can never disagree about which article is part 2 — which also
means a series term page lists oldest-first rather than newest-first, unlike
every other term page. That is the feature.
A member of several series shows one strip, for the first term it names. A series of one shows none.
Neither authors nor series appears in the generic taxonomy chip row on an
article, because each has a surface of its own. Name one in
params.taxonomy.page_header to put it back.
Share
params.ui.share puts a share bar at the top of the page end. It is empty by
default, so nothing renders until a site names its targets, in the order it
wants them:
Sixteen targets are available: x, bluesky, mastodon, facebook,
linkedin, reddit, hackernews, telegram, whatsapp, line,
pinterest, weibo, chatgpt, claude, email, and copy. An unknown name
warns and is dropped. Discord is absent on purpose: it publishes no share-intent
URL at all, so copy stands in for it rather than the theme guessing at a
private scheme.
The page key is share, so a cascade scopes the bar to one tree, a page’s own
list replaces the inherited one, and share: false opts a single page out:
Only a regular page renders the bar — a list, a term page and the home page have no single thing being shared — and print, Markdown and RSS carry none of it.
What the bar does not do is why it can ship in this theme at all. There is no
share count, no platform SDK, no iframe, and no third-party script or
stylesheet, which is what those three usually arrive as: one request per page to
a company the reader never chose. Every target is a plain <a href> intent link
carrying only the page’s own permalink and title, with no campaign parameters,
plus one local copy button. Nothing is fetched when the site builds or when the
page loads; the only request a share can cause is the navigation the reader
starts by clicking. A build with every target enabled passes
bin/check-output-security.py with no --third-party allowance.
chatgpt and claude hand that same build-time permalink to an assistant with
a prompt asking it to read the page. They are not the “open in ChatGPT” /
“open in Claude” entries of the page action menu, which the runtime rewrites at
activation time to the live browser URL and which therefore stay behind
page_context_menu.assistant_links.
The copy button is the built-in copy_link action, which means the Command
Palette carries it on every page of every site whether or not a bar is
configured.
Verify
It must reach Total in … with no ERROR and no WARN. Then confirm:
- The post appears under the right year group at
/blog/, with the date in the expected format; public/blog/index.xmlexists, contains the post, and its links are complete absolute addresses;- The thumbnail shows in the list (a missing one means none of the three featured-image sources matched);
- Tag chips lead to the corresponding tag page.
Related
- Writing pages — how to write the body
- Page parameters — the full definition of
author,imagesand the rest - Organizing content — directories and the sidebar
- Taxonomies — tags and categories
- Releases and downloads — version cards and asset tables