gohan ships with a built-in plugin system that allows optional features to be enabled per-project via config.yaml without requiring users to write Go code.
Plugins are compiled into the gohan binary. Enabling or disabling a plugin is a configuration change only — no recompilation is needed by the end user.
cmd/gohan/build.go
└── plugin.DefaultRegistry().Enrich(site) ← called between Process() and Generate()
└── for each enabled plugin:
plugin.TemplateData(article, cfg) → stored in article.PluginData["<name>"]
Template access pattern:
{{with index .PluginData "amazon_books"}}
{{range .books}}
<a href="{{.LinkURL}}">{{.Title}}</a>
{{end}}
{{end}}Defined in internal/plugin/plugin.go:
type Plugin interface {
Name() string
Enabled(cfg map[string]interface{}) bool
TemplateData(article *model.ProcessedArticle, cfg map[string]interface{}) (map[string]interface{}, error)
}Name()— unique key used inconfig.yamlunderplugins.<name>and as the key inProcessedArticle.PluginDataEnabled()— receives the plugin's config sub-map; controls whether the plugin runsTemplateData()— returns arbitrary data exposed to the theme template
Plugins read per-article data from FrontMatter.Extra, which captures all unknown YAML keys via yaml:",inline":
---
title: My Article
tags: [go]
# Plugin-specific keys:
books:
- asin: "4873119464"
title: "入門 Go"
---| Plugin | Package | Purpose |
|---|---|---|
amazon_books |
internal/plugin/amazonbooks |
Amazon book cards with affiliate tracking |
Generates book card data (image URL, product URL, title) from ASIN values declared in the article's front-matter.
config.yaml:
plugins:
amazon_books:
enabled: true
tag: "your-associate-tag-22"Article front-matter:
books:
- asin: "4873119464"
title: "入門 Go" # optional; used for alt textTemplate data shape:
.PluginData["amazon_books"].books → []BookCard
BookCard.ASIN string
BookCard.Title string
BookCard.ImageURL string # images-na.ssl-images-amazon.com CDN
BookCard.LinkURL string # amazon.co.jp/dp/{ASIN}?tag={tag}
- Create
internal/plugin/<name>/<name>.goimplementingplugin.Plugin - Add a compile-time interface check:
var _ plugin.Plugin = (*MyPlugin)(nil) - Register in
internal/plugin/registry.go→DefaultRegistry() - Document in this section
While Plugin operates on individual articles, SitePlugin operates on the full site and generates VirtualPages — pages with no corresponding Markdown source file.
cmd/gohan/build.go
└── plugin.DefaultRegistry().EnrichVirtual(site) ← called after Enrich()
└── for each enabled SitePlugin:
SitePlugin.VirtualPages(site, cfg) → appended to site.VirtualPages
↓
HTMLGenerator.buildJobs() renders them
Template access pattern (the page-specific data is at .VirtualPageData):
{{range index .VirtualPageData "books"}}
<a href="{{.LinkURL}}" target="_blank" rel="noopener">
<img src="{{.ImageURL}}" alt="{{.Title}}">
</a>
{{if .ArticleURL}}<a href="{{.ArticleURL}}">{{.ArticleTitle}}</a>{{end}}
{{end}}Defined in internal/plugin/plugin.go:
type SitePlugin interface {
Name() string
Enabled(cfg map[string]interface{}) bool
VirtualPages(site *model.Site, cfg map[string]interface{}) ([]*model.VirtualPage, error)
}Name()— unique key used inconfig.yamlunderplugins.<name>Enabled()— controls whether the plugin runsVirtualPages()— inspects the full site and returns zero or moreVirtualPagevalues
VirtualPage.OutputPath string // file path relative to output dir, e.g. "bookshelf/index.html"
VirtualPage.URL string // canonical URL path, e.g. "/bookshelf/"
VirtualPage.Template string // theme template filename, e.g. "bookshelf.html"
VirtualPage.Locale string // locale code, e.g. "en" or "ja"
VirtualPage.Data map[string]interface{} // exposed as .VirtualPageData in templates
Aggregates all book entries from every article's books: front-matter and generates one bookshelf page per locale.
config.yaml:
plugins:
bookshelf:
enabled: true
tag: "your-associate-tag-22" # Amazon Associates tracking tagArticle front-matter:
books:
- asin: "4873119464" # Amazon ASIN — generates cover image + Amazon link
title: "入門 Go"
- url: "https://booth.pm/..." # non-Amazon: direct sales URL (no cover image)
title: "My Zine"When url: is provided instead of asin:, ImageURL is empty and LinkURL is the direct URL.
Generated URLs:
- Default locale (en):
/bookshelf/ - Non-default locale (ja):
/ja/bookshelf/
Template data shape (.VirtualPageData):**
.VirtualPageData["books"] → []BookEntry
BookEntry.ASIN string
BookEntry.Title string
BookEntry.ImageURL string # images-na.ssl-images-amazon.com CDN; empty for url:-only entries
BookEntry.LinkURL string # amazon.co.jp/dp/{ASIN}?tag={tag}, or the direct url: value
BookEntry.ArticleSlug string # slug of the source article (book review)
BookEntry.ArticleTitle string # title of the source article
BookEntry.ArticleURL string # canonical URL of the source article
BookEntry.Categories []string # categories of the source article
BookEntry.Date time.Time
.VirtualPageData["categories"] → []CategoryGroup # entries grouped by category
CategoryGroup.Name string # category name; empty string = uncategorised
CategoryGroup.Books []BookEntry
books is sorted by date descending (newest first). categories is sorted alphabetically by category name; uncategorised entries appear last.
Template example (category grouping):
{{range index .VirtualPageData "categories"}}
<h2>{{if .Name}}{{.Name}}{{else}}Uncategorised{{end}}</h2>
{{range .Books}}
<a href="{{.LinkURL}}" target="_blank" rel="noopener">
{{if .ImageURL}}<img src="{{.ImageURL}}" alt="{{.Title}}">{{else}}{{.Title}}{{end}}
</a>
{{end}}
{{end}}- Create
internal/plugin/<name>/<name>.goimplementingplugin.SitePlugin - Add a compile-time interface check:
var _ plugin.SitePlugin = (*MyPlugin)(nil) - Register in
internal/plugin/registry.go→DefaultRegistry()undersitePlugins - Create a theme template that reads
.VirtualPageData - Document in this section
- Dynamic plugin loading (
pluginpackage) is intentionally out of scope — it adds OS constraints and complexity that are unnecessary for a static site generator - Plugins do not generate HTML; they supply data to themes, keeping UI fully under the theme's control
- Per-article data is read-only from the plugin's perspective
- VirtualPages are not included in Atom feeds or the index article listing