You write [[coffee]] in your notes, not a URL. v7.4.0 brings that habit to Fanstatic: wikilinks resolve to pages and taxonomy terms at build time, so links survive slug changes and content moves.
A wikilink is [[target]] or [[target|label]]. The target is a content path (posts/my-post.md, /posts/my-post.md) resolved exactly like ref, a taxonomy term (tags/coffee), or a bare name that tries a term first and then a page:
Read [[posts/how-to-brew.md]] next, or browse everything tagged [[tags/coffee]].
The [[coffee|bean]] is a seed, not a bean.
Bare targets prefer a taxonomy term, so [[coffee]] links to the coffee tag when one exists. The calling page's language and output format are the defaults, matching ref, so a wikilink in a Portuguese page points at the Portuguese translation. The label after the pipe is optional.
Wikilinks also work in shortcode Markdown and the markdownify template filter. In a Markdown table, escape the label separator as [[coffee\|bean]]. Plain text and image descriptions use just the display label.
Missing targets
A target that resolves to nothing renders a link to ./ and logs an error, and the build keeps going, so a typo never blocks a deploy. When you would rather catch it early, turn it into a hard failure:
Until now, ref and relref were the only shortcodes Fanstatic understood. v7.3.0 opens the door: any Liquid file in shortcodes/ becomes a shortcode you can call from your content, with the same syntax as Hugo.
Both Hugo notations work: {{< … >}} inserts HTML without a stray <p>, and {{% … %}} inserts Markdown that is rendered with the page. Shortcodes nest, receive typed arguments, and see inner, page and a shortcode object with the call's name, parameters, parent and position. A site file overrides the theme's, and audio.pt-br.html or audio.rss.xml take over for Portuguese pages or the RSS feed. A typo in a shortcode name fails the build with the file, line and column.
Beyond Hugo
Parameter schemas: a YAML header in the shortcode declares types, required parameters, defaults and allowed values, and every call is checked at build time.
Lists and maps: tags=["a", "b"] and size={w: 300, h: 200} arrive as real collections.
Directives: :::note{kind=warn} Title … ::: calls the note shortcode, as in Docusaurus.
Layouts: {% shortcode 'audio', src: page.Params.audio %} calls the same shortcode from a template, and page.Shortcodes tells a layout which assets a page needs.
Built-in shortcodes
details, figure, highlight, instagram, param, qr, vimeo, x and youtube ship with Fanstatic, next to ref and relref. YouTube embeds use the privacy-enhanced host, QR codes are inlined as PNGs, and a site or theme file of the same name replaces any of them. See the shortcodes guide for every argument.
Branch bundles are list pages
An _index.md file now renders as a list page with list.html and gets an RSS feed, as in Hugo, while index.md stays a single page.
A static site generator should disappear into your workflow. You save a file, the site is rebuilt, and you never think
about it. That works until your site grows, and then one innocent line in a theme starts costing you seconds on every
build.
Fanstatic is now part of the MASS4 family, and the MASS4 sites are our first customers. So for Fanstatic 7.2 we went
looking for everything that makes a build slow as a site grows, measured it on real sites, and fixed it.
Turian site, full build (486 files, 1,839 outputs)
841 ms
647 ms
-23%
Turian site, Create phase
753 ms
558 ms
-26%
4,000-post blog with a "recent posts" sidebar on every page, Create phase
2,707 ms
583 ms
-78%
4,000-post blog, Parse phase
182 ms
78 ms
-57%
Small sites were already instant, and they stay instant: the Fanstatic documentation site and the MASS4 site build in the same time as before, give or take a few milliseconds. The gains grow with the size of your site.
Why builds go quadratic
Most of a build is linear: read a file, render a page, write it. The cost explodes when every page does work proportional to all pages. A typical example is a "recent posts" sidebar:
Every page re-sorts the whole site. With N pages that is N sorts of N items: double the posts and the work goes up by more than four.
graph LR
A[Parse<br/>read files, YAML] --> B[Generate Pages<br/>sections, taxonomies]
B --> C[Create<br/>render every template]
C --> D[Write public/]
style A fill:#2b6cb0,color:#fff;
style B fill:#2b6cb0,color:#fff;
style C fill:#c05621,color:#fff;
Parse and Generate Pages grow roughly linearly. Create is where themes live, so it is where the curve bends, and
where this release spends most of its effort.
What changed
Area
What we did
Who benefits
Page collections
site.Pages and RegularPages are built once, after pages are registered, instead of re-scanned on every access
Every theme
Derived values
Plain, WordCount, TagsReference and Taxonomies are computed once per page
Themes that show word counts, reading time or tags
Page content
A page's Content is rendered once, however many lists, feeds and home pages include it
Lists, RSS, home pages
Sorted views
New cached views: RegularPagesByDate, ByLastMod, ByWeight, ByTitle (and the Pages… equivalents)
Themes that sort per page
Template cache
Compiled templates are looked up by file path instead of by hashing the template body
Every theme
Parsing
Markdown files in a folder are read and parsed in parallel, then linked in a fixed, sorted order
Large content folders
Observability
fanstatic build --template-metrics reports calls, time and cache hits per template
Theme authors
Guardrail
A benchmark target with a regression gate runs in CI
Everyone, in the future
Two details matter for correctness:
Cycles are reported, not deadlocked. If page A's content reads page B's content and B reads A's, the build now stops with a clear error instead of hanging.
Ordering is deterministic. Pages with equal weight are ordered by source path, so two builds of the same input give the same output. This can change the order of equal-weight lists compared with 7.1.1, for example home-page cards or RSS items.
Benchmarks on real MASS4 sites
We built three real sites with the same compiler flags, once with the 7.1.1 source and once with 7.2.0. Each number is the median of 5 builds after a warm-up, in milliseconds, as printed in Fanstatic's own build report.
Site
Markdown files
Outputs
Phase
7.1.1
7.2.0
Change
Turian
486
1,839
Parse
45
40
-11%
Create
753
558
-26%
Total
841
647
-23%
Fanstatic docs
85
247
Create
101
105
within noise
Total
154
162
within noise
MASS4
13
57
Create
46
46
none
Total
90
96
within noise
The pattern is what you would expect from the changes above: sites with hundreds of pages and shared layouts win, and sites with a few dozen pages have nothing left to save.
Turian is the interesting one. Its pages share a base layout and many list pages, which is exactly where cached content and cached collections pay off. And there is more to win: see Find your own slow template.
Benchmarks on synthetic blogs
To see how builds scale, we generated blogs with 1,000, 2,000 and 4,000 posts, three tags per post, and a "recent posts" sidebar with word counts on every post page. Create phase, median of 5 builds, in milliseconds:
Posts
7.1.1, sort in the theme
7.2.0, same theme
7.2.0, cached view
Change with cached view
1,000
288
234
101
-65%
2,000
1,005
808
208
-79%
4,000
2,707
2,421
583
-78%
xychart-beta
title "Create phase with a sidebar on every page (ms, lower is better)"
x-axis ["1,000 posts", "2,000 posts", "4,000 posts"]
y-axis "milliseconds" 0 --> 3000
line [288, 1005, 2707]
line [234, 808, 2421]
line [101, 208, 583]
The three lines are, from top to bottom: 7.1.1, 7.2.0 with an unchanged theme, and 7.2.0 with a theme that uses the cached view. Two things stand out:
An unchanged theme gets 10 to 20% faster. The sort itself is still there, so the curve is still steep.
A theme that uses the new cached view cuts the cost by roughly three to five times and flattens the curve a lot.
For comparison, the same blogs without a sidebar:
Posts
Parse 7.1.1
Parse 7.2.0
Create 7.1.1
Create 7.2.0
Total 7.1.1
Total 7.2.0
1,000
54
30
59
57
163
144
2,000
95
47
71
74
225
185
4,000
182
78
99
99
352
274
Without a quadratic pattern in the theme, Create was already flat. Parallel parsing makes the Parse phase 1.8 to 2.3 times faster.
For theme authors: stop sorting on every page
The new views are ordered ascending, are computed once, and are shared by every page:
If your theme sorts site.RegularPages by
Use
'Date'
site.RegularPagesByDate
'Lastmod'
site.RegularPagesByLastMod
'Weight'
site.RegularPagesByWeight
'Title'
site.RegularPagesByTitle
The same four views exist on a page: page.RegularPagesByDate, page.PagesByDate, and so on.
For a "newest first" list, use the reversed and limit modifiers of for:
{# before: sorts and reverses the whole site on every page #}
{% assign recent = site.RegularPages | sort: 'Date' | reverse %}
{% for post in recent limit: 5 %}...{% endfor %}
{# after #}
{% for post in site.RegularPagesByDate reversed limit: 5 %}
<a href="{{ post.RelPermalink }}">{{ post.Title }}</a>
{% endfor %}
Prefer for ... reversed limit: N over | reverse. The reverse filter still copies the whole list on every page. The views need Fanstatic 7.2 or later; an older binary will not know them and renders an empty list.
Find your own slow template
Add --template-metrics to a build and Fanstatic prints one row per template, sorted by total time:
fanstatic build --template-metrics
This is the report for the Turian site:
Template
Calls
Total
Max
Cache hits
_default/baseof.html
917
10,943 ms
129 ms
917
docs/single.html
152
2,834 ms
74 ms
152
_default/single.html
299
164 ms
13 ms
299
blog/single.html
29
86 ms
10 ms
29
(inline templates)
4,780
69 ms
6 ms
4,658
Times are inclusive and summed over all threads, so they add up to more than the wall-clock build time. Partials rendered with {% render %} count towards the template that calls them. A page's Content is charged to the first template that reads it. For Turian, the report points straight at the documentation layout, which still sorts and scans all pages for every documentation page. That is a ready-made to-do list for its theme.
A guardrail so it stays fast
Performance work rots when nobody is watching. The repository now has a Benchmark target that generates synthetic blogs with 1,000, 2,000 and 4,000 posts, builds each one with the freshly published binary, and fails if the Parse or Create phase becomes more than 25% slower than the stored baseline.
graph TD
A[Pull request] --> B[Publish Release binary]
B --> C[Generate 1,000 / 2,000 / 4,000 post blogs]
C --> D[Build each site 5 times]
D --> E{Median Parse or Create<br/>more than 25% slower<br/>than baseline?}
E -- no --> F[Job passes]
E -- yes --> G[Job fails]
D --> H[Upload benchmark.json]
The first version of this gate is deliberately conservative: its baseline was recorded on a developer machine, so the CI job is not a required check yet. We will switch to a baseline recorded on the CI runner before we rely on it.
Good to know
How we measured. .NET SDK 10.0.112, one machine, medians of 5 builds after a warm-up, using Fanstatic's own Parse, Create and Total figures. 7.1.1 and 7.2.0 were built from source with identical publish flags. Your hardware and theme will give different absolute numbers; the shape of the curves is the point.
7.2.0 contains more than speed. It also ships configurable taxonomies, so pages that show tags and categories can differ from 7.1.1. Read the taxonomy guide when you upgrade.
Equal-weight ordering is now stable. Lists that relied on an accidental order may come out differently once; they will not change again from build to build.
What's next
Cached views remove the sort, but Liquid still wraps a list into its own array type every time a template reads it. For a 4,000-post site that is the remaining 583 ms in the table above, against 99 ms with no sidebar. Caching that wrapper is the next step, followed by incremental rebuilds in serve, so that editing one page does not rebuild the site.
Try it, run --template-metrics on your own site, and tell us what you find.
A broken Liquid template used to cost you an afternoon: one error per rendered page, no file, no line, and a build that still reported success. Fanstatic v7.2.0 fixes that, and closes two more bugs that were quietly corrupting your public/ folder and your build report.
Syntax errors caught during theme compilation look the same:
[ERR] 'themes/my-theme/_default/single.html:12:3': 'unknown_tag' is not a known tag
{% unknown_tag %}
Templates are compiled before rendering starts, so a typo in your theme is reported immediately instead of once per page. Errors that only surface at render time — a missing partial, a bad filter — are located by walking back to the calling tag in the template body, which is why the line and column are still exact. When one broken template affects hundreds of pages, it is logged a single time with the first page that hit it, so the output stays readable.
A broken theme now fails the build
fanstatic build exits with a non-zero status code when template errors were found, so a broken theme can no longer
pass unnoticed in CI or in a deploy script.
# Still exits with 0 and reports every error
fanstatic build --continue-on-error
While fanstatic serve is running, template errors are additionally overlaid on the generated page in your browser, with the message and the offending tag in place of the broken output.
Nested static folders are copied
static/ folders were copied flat: every file landed directly in the root of the output, so static/css/site.css was published as /site.css and could silently overwrite a file of the same name from another folder. Site and theme static folders are now copied recursively, keeping their structure:
The Parse and Generate Pages rows of the build report were reading the same counter, and that counter counted index files plus content sources — not Markdown files, and not pages. Parse now equals the number of Markdown files read, and Generate Pages the number of pages actually created, including the section, taxonomy, and term pages that Fanstatic generates for you:
Step Quantity Duration
─────────────────────────────────────────────
Parse 83 18 ms
Generate Pages 181 39 ms
Configurable taxonomies
Tags keep their existing behavior, and categories are enabled by default. Sites can now configure custom taxonomies, with term pages and metadata available to templates. Migrate tag links to page.Taxonomies.tags and build tag clouds from site.Taxonomies.tags, which exposes each term's URL and page count. The existing page.Tags and page.TagsReference properties remain available during migration. See the taxonomy guide for configuration and examples.
Faster builds
Large sites build up to 23% faster, themes get cached sorted views such as site.RegularPagesByDate, and
fanstatic build --template-metrics shows which template is slow. Benchmarks on the MASS4 sites, charts, and a theme
migration guide are in Faster Builds, Flatter Curves.
Fanstatic is the new name for the static site generator you may have known as SuCoS. If you've used SuCoS, you already know Fanstatic — the engine, the CLI, the Liquid templates, and every feature built up over six and a half years of releases carry straight over. What changes with v7.0.0 is the name, the home, and the role: this is no longer a personal project with a side hustle, it's first-class tooling under the MASS4 organization.
SuCoS — "Tastiest C# Static Site Generator of the World" — was a personal project from day one, built to get better at C# and to stop fighting Hugo's templating. Over nine major versions it grew Markdown extensions, sections and taxonomies, multi-language support with full i18n, ref/relref shortcodes, and a validate-links command that understands local versus production URLs. Somewhere along the way it quietly became the tool every other project reached for, MASS4's sites included. Fanstatic — for Fans of a Fantastic Static Site Generator — is that same tool, renamed to match what it actually is now: shared infrastructure, not a side project. New source home at github.com/MASS4ORG/fanstatic, still MIT licensed.
What's still here
Nothing was left behind in the move. Fanstatic v7.0.0 is the direct continuation of SuCoS v6.9.0, with the same CLI surface:
fanstatic new-site <YOUR_SITE_PATH> # To create your site
fanstatic <YOUR_SITE_PATH> # To build your site
fanstatic serve <YOUR_SITE_PATH> # To build and run locally
fanstatic validate-links <YOUR_SITE_PATH> # To check all links
fanstatic api <YOUR_CODE_PATH> <YOUR_SITE_PATH> # To generate API reference
That includes Liquid templating, Hugo-style shortcodes, full i18n with per-language sections and translation strings, smart validate-links local-baseurl resolution, and — as of the last SuCoS release before the rename — a native Windows installer.
What's new since v7.0.0
The rename shipped alongside the work already in flight: v7.0.1 fixed the Windows installer placing fanstatic correctly on the user PATH, and v7.0.2 added a winget manifest, so Windows users can now install with:
winget install MASS4ORG.Fanstatic
Get started
Grab a release from github.com/MASS4ORG/fanstatic for Linux or Windows, or install via winget on Windows. Everything you already know from SuCoS applies — the docs on this site are the same docs, just living at a new address.
v6.8.0 Release: Smart Shortcodes and Link Validation
SuCoS v6.8.0 introduces highly convenient Hugo-style shortcodes for internal link management alongside major improvements to link validation efficiency. This update makes it easier to reference your internal pages dynamically while ensuring local test environments don't trigger unnecessary external requests.
We've added native support for ref and relref shortcodes directly within your Markdown content. This allows you to resolve page permalinks dynamically without hardcoding URLs.
Check out our [About Page]({{< ref "path/to/page.md" >}}) for absolute links, or link relatively using [this section]({{< relref "path/to/page.md#anchor" >}}).
By default, the shortcodes automatically resolve using the calling page's language and output format context. However, you can explicitly override these when needed by appending lang="..." or outputFormat="..." parameters. It also fully supports #anchor fragment resolution out of the box.
Smarter Link Validation
The validate-links engine received a robust intelligence upgrade focused on local testing workflow and edge-case handling:
Local BaseUrl Mapping: Absolute links containing your production domain (e.g., hardcoded production URLs) are now smart-resolved against your local site pages during testing instead of initiating actual external HTTP requests. If you prefer the legacy behavior, you can append the -b or --strict-baseurl flags to enforce exact domain-matching.
Hash-Routed URL Fix: Fixed a routing bug where root-path external URLs using hashes (such as Matrix links like [https://matrix.to/#/!room:matrix.org](https://matrix.to/#/!room:matrix.org)) were mistakenly flagged as internal page hits. This previously triggered frustrating, false-positive fragmentNotFound errors on the home page.
SuCoS v6.8.0 introduces highly convenient Hugo-style shortcodes for internal link management alongside major improvements to link validation efficiency. This update makes it easier to reference your internal pages dynamically while ensuring local test environments don't trigger unnecessary external requests.
We've added native support for ref and relref shortcodes directly within your Markdown content. This allows you to resolve page permalinks dynamically without hardcoding URLs.
Check out our [About Page]({{< ref "path/to/page.md" >}}) for absolute links, or link relatively using [this section]({{< relref "path/to/page.md#anchor" >}}).
By default, the shortcodes automatically resolve using the calling page's language and output format context. However, you can explicitly override these when needed by appending lang="..." or outputFormat="..." parameters. It also fully supports #anchor fragment resolution out of the box.
Smarter Link Validation
The validate-links engine received a robust intelligence upgrade focused on local testing workflow and edge-case handling:
Local BaseUrl Mapping: Absolute links containing your production domain (e.g., hardcoded production URLs) are now smart-resolved against your local site pages during testing instead of initiating actual external HTTP requests. If you prefer the legacy behavior, you can append the -b or --strict-baseurl flags to enforce exact domain-matching.
Hash-Routed URL Fix: Fixed a routing bug where root-path external URLs using hashes (such as Matrix links like [https://matrix.to/#/!room:matrix.org](https://matrix.to/#/!room:matrix.org)) were mistakenly flagged as internal page hits. This previously triggered frustrating, false-positive fragmentNotFound errors on the home page.
SuCoS v6.7.0 is the biggest release yet, introducing full multi-language and internationalization support. Whether your site needs two languages or twenty, SuCoS now has you covered. For a complete reference, see the Localization documentation.
SuCoS now supports building sites in multiple languages. Configure your languages in the site settings:
languages:
en:
LanguageName: English
Weight: 1
Title: "My Site"
IsDefault: true
pt-br:
LanguageName: "Português (Brasil)"
Weight: 2
Title: "Meu Site"
Content is mapped to languages through filename suffixes. Simply name your files with the language code (e.g., hello.pt-br.md) and SuCoS automatically associates them with the correct language. Non-default languages get language-prefixed URLs (/pt-br/hello/), while the default language keeps clean URLs by default. Set DefaultContentLanguageInSubdir: true to prefix all languages uniformly.
Auto-generated section and taxonomy pages are now created for every configured language, not just the default. If a directory has an _index.md only in English, SuCoS automatically synthesizes stubs for the other languages — no manual duplication required.
Internationalization (i18n)
Bring your templates to life in any language with the new i18n system. Create translation files in i18n/ directory:
If there is a single page, this renders 1 post; with ten pages, it renders 10 posts.
When a translation key is missing in the current language, SuCoS falls back to the default language automatically — so you can roll out translations incrementally.
Translation-Aware Templates
Building language switchers and cross-language navigation is now straightforward with new page properties:
page.Translations — same-page content in other languages, perfect for language switchers
page.AllTranslations — all translations including self, sorted by language weight
page.TranslationsByLanguage — translations keyed by language code
page.Variants — every representation across all languages and output formats
page.AlternativeOutputFormats — output formats (HTML, RSS, JSON) for the same language
page.IsDefaultLanguage — check if page is in the site's default language
site.Languages — list of all configured language settings
site.Language — the current language context for this output
Here's a complete language switcher:
{% if page.Translations.size > 0 %}
<nav class="language-switcher">
{% for t in page.Translations %}
<a href="{{ t.RelPermalink }}" lang="{{ t.Language }}">
{{ t.LanguageName }}
</a>
{% endfor %}
</nav>
{% endif %}
Bug Fixes
This release also fixes several important issues:
Section page parent references: Auto-generated section pages (directories without _index.md) now correctly inherit the section as parent instead of home, preventing URL collisions between identically-named files across directories.
Resource URL generation: Fixed when RelPermalinkDir lacks a trailing slash (e.g., root /).
Resource Name/Title rendering: Liquid template rendering now uses SiteInternal instead of the SiteOutput facade.
Debian publishing: The .deb package is now correctly published to the generic package registry and attached as a release link.
SuCoS v6.6.0 focuses on making the generator easier to install and more efficient to distribute. We've managed to significantly shrink the binary size and improve our packaging for Linux users.
One of the biggest changes in this release is the introduction of executable compression. By enabling compression in our build pipeline, we've managed to reduce the output size of the SuCoS executable from approximately 100MB down to just 20MB.
This makes downloading and deploying SuCoS much faster, without sacrificing any of the performance you've come to expect.
Debian/Ubuntu Support
We are happy to announce that we are now officially generating .deb packages for every release! If you are on Debian, Ubuntu, or any derivative distribution, you can now install SuCoS using your favorite package manager.
This ensures that SuCoS is properly integrated into your system, making it easier to manage updates and dependencies.
SuCoS v6.5.0 is a massive update that brings highly requested pagination features and a significant performance boost. This release reimagines how lists and sitemaps are handled, making it easier than ever to manage large amounts of content.
We've introduced a new, flexible way to handle pagination directly in your Liquid templates. With the new paginate filter, you can now decide exactly how and when to split your content.
The new Pager record exposes everything you need: Count, Current, First, Last, Prev, Next, and more. Plus, we've included a built-in partials/pagination.html so you can get started instantly without writing a single line of pagination UI logic.
Built-in Sitemap Support
Keeping search engines happy is now easier than ever. SuCoS now includes a built-in sitemap.xml generator. By default, the home page will now also generate a sitemap using a built-in Liquid template.
To support this and other cross-format needs, we've added site.AllRegularPages, which provides a complete list of all HTML regular pages across all output formats. This is perfect for generating sitemaps, RSS feeds, or custom search indexes.
The template lookup has also been improved to be more specific, checking for format-specific paths like _default/sitemap.xml before falling back to generic templates. This ensures your sitemap looks exactly like a sitemap should, without interference from your standard list templates.
Massive Performance Gains
Performance is at the core of SuCoS, and v6.5.0 takes it to the next level. We've identified and eliminated several bottlenecks:
Template Caching: Compiled Fluid templates are now cached, eliminating the biggest single overhead in the rendering pipeline.
Content Caching: Markdig HTML rendering is now cached per page, preventing redundant processing during pagination.
Optimized Internals: Core path handling and URL sanitization have been rewritten to avoid unnecessary allocations, and we've adopted FrozenDictionary for lightning-fast lookups.
These changes result in a much snappier experience, especially for sites with hundreds or thousands of pages.
Improved Stability
This release also fixes a long-standing issue in serve mode where certain file system events could trigger infinite site recreation. By reimplementing our file provider, we've made the development loop more reliable and responsive.