Open-source project
joway/hugo-theme-yinyang avatar
joway/hugo-theme-yinyang

YinYang: a black and white Hugo theme with images handled properly

A black-white theme for Hugo.

559 stars164 forksHTMLMIT

At a glance

What is it?
YinYang is a minimalist black and white theme for Hugo, MIT licensed and built around Hugo extended 0.158 or newer. Its distinguishing feature is that images under assets/ are converted to WebP with a srcset, while images under static/ are served unchanged, and it keeps its own SEO partial, multilingual support and a fairly large set of content hooks.
Who is it for?
Adopt YinYang if you want a deliberately plain Hugo theme that still handles the unglamorous parts properly, particularly image formats and per post metadata. Do not adopt it if you want a feature heavy theme, because the whole pitch is minimalism and the configuration surface is a single params block.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 41 days ago.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A theme with a stated floor

YinYang is described as a black-white theme for Hugo, and the readme leads with a requirement block that is more specific than most themes bother with.

The requirement is Hugo extended >= 0.158, with the reason given: older versions fail on .Language.Label and .Site.Language.Locale. Those two template properties are what a multilingual theme needs, and the readme is telling you that the theme uses them rather than working around them for older Hugo versions.

The extended edition is the one with the SCSS and Sass pipeline, so a theme requiring it is making a deliberate choice about how styles are built rather than restricting itself to plain CSS.

The rest of that block covers three behaviours that are easy to miss and expensive to debug later. Images under assets/images/ are converted to WebP with a srcset, and images under static/ are served unchanged. The lazyImage and staticPrefix parameters are no longer needed, because native loading="lazy" is used, and they are ignored. And setting params.extraHeadProductionOnly = true skips the extraHead block during hugo server.

That last one is a small piece of developer ergonomics that shows the author has used the theme. extraHead is the standard place to put analytics snippets and third party scripts, and running those against a local development server is a good way to pollute your own data with development traffic. The flag means the production-only head content is skipped while you work.

The feature list is four items: minimalist, multi-language support, Disqus support, and SEO optimization, with the SEO partial linked directly to its source in layouts/partials/seo.html. The theme is MIT licensed, the primary language is HTML, and there is a demo at blog.joway.io along with a screenshot in the repository. The last push was on 2026-09-12.

Installation and the two-line setup

The theme is installed as a git submodule, which is the approach Hugo still recommends for themes rather than copying the directory into your site.

From the root of your site:

shell
git submodule add https://github.com/joway/hugo-theme-yinyang.git themes/yinyang

Then one line in config.toml:

toml
theme = "yinyang"

A submodule is a good fit here for a reason beyond convenience. The theme lives in its own repository with its own history, so pulling in an update is a submodule update rather than a copy of someone else's files into your tree, and a diff of what changed in the theme is available without diffing against a vendored copy.

The repository layout is the standard Hugo theme layout, with archetypes/, assets/, layouts/, static/ and an exampleSite/ directory, plus theme.toml, images/ and a LICENSE.md. The exampleSite is worth a look, because it is a complete working configuration that you can copy rather than assemble from the readme.

The configuration surface is a single params block, and it is small enough to read in one go. The head title falls back to the site author name when it is not set:

code
[params]
headTitle = "Joway Wang"

The main section is a list:

code
[params]
mainSections = ["posts"]

And there are hooks for extra head content and extra CSS files, both of which are strings and lists under params, which is the mechanism for the custom analytics or font snippet most sites need without forking the theme.

The readme is explicit that if there is no headTitle in params, the theme uses .Site.Params.author.name, which is the kind of small documented fallback that saves you from a blank title tag when you first install it.

Multilingual content and how it is organised

Multi-language support is listed as a feature, and the configuration is standard Hugo with the content directories separated per language.

code
[languages]
  [languages.en]
    contentDir = "content/en"
    languageName = "English"
    weight = 1
  [languages.cn]
    contentDir = "content/cn"
    languageName = "Chinese"
    weight = 2

Posts then go into content/en or content/cn, and the weight value decides which language is the default when one has to be picked. The contentDir per language is the arrangement that works well with a repository where each language is a directory of Markdown files, rather than using the filename suffix convention of content/post.en.md.

The full example configuration at the end of the readme shows how this sits alongside the rest of a real site. It sets baseURL, languageCode, title, DefaultContentLanguage, canonifyURLs and enableRobotsTXT, then the languages block with the English one commented out and Chinese active. There is a permalinks section setting post to /blog/:title/.

The markup section is the part that catches people. It enables Goldmark, the default CommonMark parser in modern Hugo, and sets unsafe = true under the renderer options, which is what allows raw HTML in your Markdown to be rendered. It then configures highlighting with guessSyntax = true, noClasses = false, the tango style and a tab width of 2. noClasses = false means the syntax highlighting classes are written into the HTML rather than relying on a separate stylesheet, which is what makes a single theme work without shipping a highlight stylesheet.

The example also sets the author block with a homepage and a name, and shows how social links are declared as an array of tables, one per entry, with a name and a link. There is a commented-out block for an RSS feed and other links, which is a useful illustration that the socials list is entirely yours to define.

Per post content, social links and Twitter Cards

The content hooks are where a minimal theme does most of its customisation work, and YinYang has three of them.

The first is postHeaderContent and postFooterContent, which are inserted around every post. The footer example in the readme is a newsletter subscription paragraph with a link, and the header is an empty string by default. Because both are plain HTML in the configuration, they can be anything you can put in a string, including a script tag, which is how people usually slot in a paywall widget or an advertisement without touching the theme.

The second is the socials list, declared as an array of tables:

code
[[params.socials]]
name = "About Me"
link = "https://joway.io"
[[params.socials]]
name = "Github"
link = "https://github.com/joway"

The order in the configuration is the order in the footer, and the example configuration shows a fuller set with Subscribe, Travel and About entries.

The third is Twitter Cards, which needs two things. In params, twitterCards = true. And in a post's front matter, an images keyword with a list of URLs that will be used for the card.

That front matter approach is the sensible design. The card images belong to a specific post, so putting them in the post rather than in the site configuration means adding a new post with a card does not require touching the site config at all. It also means a post with several images can list all of them and let the theme and the social platform pick.

The readme also points at an optional related posts section, with includeNewer, threshold, toLower and an indices array naming date and keywords with weights. That block is commented out in the example, which is the right default for a theme, since related post selection is a matter of taste and the weights are meaningless without knowing your content.

Disqus support is configured with a disqus account name under params, and the analytics block is a standard Google Analytics services entry with an id.

The image pipeline, and why assets and static differ

The image handling is the most interesting technical decision in the theme, and it is easy to miss because it is described in a single line of the requirements block.

Images under assets/images/ are converted to WebP with a srcset. Images under static/ are served unchanged.

That distinction is the whole point of the Hugo asset pipeline. Files in assets/ are processed at build time, so the theme can look at them, convert them, and emit derivatives into the published site. Files in static/ are copied verbatim, byte for byte, with no processing at all.

So the same image placed in two different directories behaves completely differently. In assets/images/ you get a WebP version and a srcset so the browser picks a size, which is the modern answer to serving a large photo to a phone. In static/ you get exactly the file you put there, which is what you want for something that must not be touched, a file with a fixed hash in its name, a video, a font, or an image you are deliberately shipping at a specific quality.

Converting to WebP with a srcset also means the site stops serving the original format to every browser. For a blog with photographs, that is often the single largest performance win available, and doing it in the theme means it happens without the author having to think about it.

The removed parameters explain themselves in the same way. The lazyImage and staticPrefix parameters are no longer needed, because native loading="lazy" is used and they are ignored. lazyImage was the old approach, wrapping images in a placeholder and swapping them in with a small script, and native lazy loading does the same job with no JavaScript. staticPrefix pointed at a CDN prefix for static assets, and the theme no longer has that concept.

A theme that ignores its old parameters rather than erroring on them is doing you a favour, because your existing configuration keeps working after an upgrade.

The remaining parts of the configuration are the small ones. extraHead injects raw head content, extraCSSFiles takes a list of paths, and both are documented separately so you can add a font or a small stylesheet without forking.

Editorial conclusion

Adopt YinYang if you want a deliberately plain Hugo theme that still handles the unglamorous parts properly, particularly image formats and per post metadata. Do not adopt it if you want a feature heavy theme, because the whole pitch is minimalism and the configuration surface is a single params block. Before you commit, check that you are on Hugo extended 0.158 or newer, since older versions fail on two language properties the theme relies on, and decide whether your images live under assets or static, because that decision changes how they are served.

Frequently asked questions

What is YinYang?

It is a black-white theme for Hugo, described as minimalist, with multi-language support, Disqus support and its own SEO partial at layouts/partials/seo.html. It is MIT licensed, its primary language is HTML, and there is a demo at blog.joway.io.

How do I install the YinYang Hugo theme?

Add it as a git submodule from the root of your site with git submodule add pointing at the repository and the themes/yinyang path, then set theme = "yinyang" in config.toml. The repository also ships an exampleSite directory with a complete working configuration.

What version of Hugo does YinYang need?

Hugo extended 0.158 or newer. The readme explains that older versions fail on .Language.Label and .Site.Language.Locale, the two template properties the multilingual support relies on. The extended edition is required rather than the standard one.

How does YinYang handle images?

Images placed under assets/images/ are converted to WebP with a srcset, so the browser can pick a suitable size. Images under static/ are served unchanged, which is what you want for files that must not be processed. The old lazyImage and staticPrefix parameters are no longer needed, since native loading="lazy" is used, and they are ignored.

How do I add social links and Twitter Cards to YinYang?

Social links are an array of tables under params.socials, each with a name and a link, and their order in the configuration is their order in the footer. Twitter Cards need twitterCards = true in params, plus an images keyword in a post's front matter holding the list of image URLs used for the card.

Official sources

  1. Issues
  2. joway/hugo-theme-yinyang on GitHub
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/joway-hugo-theme-yinyang.svg)](https://hysenlabs.com/projects/joway-hugo-theme-yinyang)

Was this useful?