一个会说 C# 的静态网站生成器:Statiq 入门终极指南
1. 简介
多年来,静态网站生成器(SSG)的世界一直被 .NET 之外的生态系统所主导。我们只能在场外看着 JavaScript 开发者享受 Gatsby 和 Next.js,Go 开发者拥抱 Hugo。对于 .NET 开发者来说,选择往往很有限、过时,或者需要离开我们舒适的 C# 环境去与不熟悉的构建链搏斗。
Statiq 应运而生。
Statiq 不仅仅是另一个静态网站生成器;它是一个专为 .NET 生态系统设计的强大、灵活的框架。它允许你使用你已经了解并喜爱的语言、库和模式——Razor、Markdown 和 C#——来构建极其快速的静态网站。无论你是在构建一个简单的博客、文档网站,还是复杂的企业 Web 应用程序,Statiq 都能将 .NET 的稳健性带入 JAMstack。
在本指南中,我将详细介绍 Statiq 到底是什么,为什么它是 .NET 开发者的游戏规则改变者,以及你如何在几分钟内启动并运行你的第一个网站。
在最后,我将为你提供一个包含以下完整功能的完整网站和主题!
- 搜索功能
- 像博客系统一样在网站中使用标签
- 添加 Google AdSense 和 Analytics 服务
- 归档页面
- 分类的列表页面
2. 为什么我们需要生成静态网站?
也许你会想,我们为什么要这样做?我们可以轻松地使用 WordPress 创建一个漂亮的网站。是的,你可以!但是要托管一个 WordPress 网站,你需要处理以下问题:
- 主机需要支持 PHP(我认为这可能不是问题)
- 性能问题(带有大量的插件)
- 安全问题。所有动态网站都有被黑客攻击的风险,因为它们可能存在一些未知的漏洞,或者你可能没有及时更新它们。(我的 WP 网站以前就被黑过 🥲)
然而相反的是,你会发现使用静态网站有以下好处:
- 你可以使用只支持 HTML 的非常便宜的主机
- 你可以获得非常好的性能
- 你的网站会非常安全,因为静态网站完全没有漏洞,如果你的主机只支持 HTML,那么对方将很难进行注入和其他攻击(这就是我喜欢的 😀)
- 你可以轻松地将任何 HTML 模板转换为你的主题
- 纯 HTML 静态网站对 SEO 非常友好
3. 什么是 Statiq?
它的核心在于,Statiq 是建立在现代 .NET 平台上的一个强大、灵活的静态生成框架。如果你来自 JavaScript(Gatsby、Next.js)或 Go(Hugo)的世界,你可以把 Statiq 看作是 .NET 中的同类产品,但在架构上有重大的不同。
虽然大多数静态网站生成器(SSG)是通过 YAML 或 TOML 文件配置的“工具”,但 Statiq 被设计为一个“框架”。这意味着你不仅仅是在“配置”你的网站;你是在“编程”它。
哲学:管道和模块
Statiq 围绕着文档(documents)、管道(pipelines)和模块(modules)这一独特的概念运作:
- 万物皆文档:在 Statiq 中,“文档”不仅仅是一个 Markdown 文件。它可以是一张图片、一个数据文件、一个 Razor 模板,甚至是一个 API 响应。
- 管道:你定义管道来处理这些文档。管道本质上就是一系列步骤的组合。
- 模块:代表管道中的单个步骤。例如,你可能有一个用来 ReadFiles(读取文件)的模块,另一个用来 RenderMarkdown(渲染 Markdown)的模块,以及最后一个用来 WriteFiles(写入文件)的模块。
这种架构让你能够对内容的获取、转换和生成方式进行精细控制。
为什么选择 Statiq?
- .NET 生态系统:你可以利用 C# 和 NuGet 包的全部力量。如果你需要在构建过程中从数据库获取数据或调用自定义 API,你只需编写 C# 代码即可完成。
- Razor & Markdown:它拥有一流的 Razor(ASP.NET Core 中使用的视图引擎)和 Markdown 支持,允许你在用纯文本编写内容的同时,构建带有逻辑的复杂布局。
- Statiq.Web:虽然核心框架是通用的,但 Statiq.Web 是建立在其之上的专用库。它预配置了用于资产、内容和模板的通用管道,让你只需最少的代码就能启动并运行一个标准网站。
Statiq 中的工作流

简而言之,Statiq 弥合了静态网站的简单性与 .NET 应用程序的动态能力之间的差距。
4. 创建并支持多主题
Statiq 非常易于使用,你可以按照官方指南在此处创建你的第一个应用。我将向你展示更多关于如何创建自己的主题以及使用 Statiq 构建完整网站的细节!😁
4.1 多主题
默认情况下,Statiq 官方指南仅提供如下简单且单一的主题结构。

但我想支持多个主题,并且能在 appsettings.json 中轻松使用它们。
为了支持多主题,我们需要创建一个 themes 文件夹,并在其中创建一个具体的主题文件夹,将模板页面放入该主题的 input 文件夹中(实际上,这不是必须的;你也可以将页面放在主题根目录中)。
在项目根目录下创建包含以下内容的 appsettings.json:
{
"Theme": "themes/editorial"
}在创建 Boststrapper 时,于 Program.cs 中读取配置:
return await Bootstrapper
.Factory
.CreateWeb(args)
.ConfigureSettings(settings =>
{
if (!settings.ContainsKey("Theme"))
{
settings["Theme"] = "themes/default";
}
})并在 Program.cs 的 ConfigureFileSystem 中处理主题模板文件:
.ConfigureFileSystem((fileSystem, settings) =>
{
var themePath = settings.GetString("Theme");
if (!string.IsNullOrEmpty(themePath))
{
// Create a validated path from the setting
var themePathNormalized = new NormalizedPath(themePath);
// Ensure the path is absolute. If it's relative, combine it with the root path.
if (themePathNormalized.IsRelative)
{
themePathNormalized = fileSystem.RootPath.Combine(themePathNormalized);
}
// Check if the theme has an 'input' folder (common convention)
// If it does, strictly use that as the input path instead of the theme root
var themeInputPath = themePathNormalized.Combine("input");
if (fileSystem.GetDirectory(themeInputPath).Exists)
{
fileSystem.InputPaths.Add(themeInputPath);
}
else
{
fileSystem.InputPaths.Add(themePathNormalized);
}
}
})就是这样,然后我们就可以随时切换主题了。
4.2 创建主题布局
好的,接下来,我将基于 HTML 5UP 的一个免费 HTML 模板来创建一个 Statiq 主题。
我们可以先看一看这个 HTML 模板。

根据这个布局,我们应该创建如下几个页面:
_Layout.cshtml:主结构Index.cshtml:主页_Sidebar.cshtml:侧边栏
这些是主要页面,但我们还需要更多,例如归档、标签、搜索和 404 页面……
所以主题文件夹结构应该如下所示。

我已经创建了一个基于 Statiq 的网站 Tableware.com。
因此,我将使用这个网站来解释如何创建主题。
首先,我们在 _Layout.cshtml 中定义主要的布局结构:
<div id="wrapper">
<div id="main">
<div class="inner">
<header id="header">
<a href="/" class="logo"><strong>Tableware</strong></a>
@* <ul class="icons">
<li><a href="#" class="icon brands fa-twitter"><span class="label">Twitter</span></a></li>
<li><a href="#" class="icon brands fa-facebook-f"><span class="label">Facebook</span></a></li>
<li><a href="#" class="icon brands fa-instagram"><span class="label">Instagram</span></a></li>
</ul> *@
</header>
@RenderBody()
</div>
</div>
@await Html.PartialAsync("_Sidebar")
</div>然后创建侧边栏,因为代码太多了,所以我只展示如何在视图中获取文章的核心逻辑,不过别担心,我会在最后提供所有源代码:
<div class="mini-posts">
@{
// 1. Get all posts in the "posts" folder
// 2. Filter out posts without a date
// 3. Order by date in descending order
// 4. x.ContainsKey("Date"): only find the post with "Date" metadata,
// exclude pages like "About Us" without date
// 5. !x.Source.IsNull: exclude pages like search-index.json
// 6. Make sure the source contain "posts"
var recentPosts = OutputPages
.Where(x => x.ContainsKey("Date") && !x.Source.IsNull && x.Source.ToString().Contains("posts"))
.OrderByDescending(x => x.Get<DateTime>("Date"))
.Take(3);
}
@if (recentPosts.Any())
{
foreach (var doc in recentPosts)
{
<article>
@if (doc.ContainsKey("Image"))
{
<a href="@doc.GetLink()" class="image">
<img src="@doc.GetString("Image")" alt="@doc.GetString("Title")" />
</a>
}
<h3 style="font-size: 1em;">
<a href="@doc.GetLink()">@doc.GetString("Title")</a>
</h3>
<p style="font-size: 0.8em; margin-bottom: 0;">
@(doc.Get<DateTime>("Date").ToLongDateString())
</p>
</article>
}
}
else
{
<p>No updates yet.</p>
}
</div>
<ul class="actions">
<li><a href="/archives" class="button">More Archives</a></li>
</ul>
</section>你可以从 input/post 文件夹中获取文章,假设这些文章是使用 Markdown 格式创建的。
以及在主页(index.cshtml)上用于展示如何获取并显示文章的代码片段:
<div class="posts">
@{
// 1. Scan all output pages
// 2. Filter condition: source file not null + source file path contains "posts" + must be .md file (exclude index.cshtml)
// 3. Sort: use Get<DateTime> to sort by date in descending order
var recentPosts = OutputPages
.Where(x => x.Source != null
&& x.Source.ToString().Contains("posts")
&& x.Source.Extension == ".md"
&& x.ContainsKey("Date"))
.OrderByDescending(x => x.Get<DateTime>("Date"))
.Take(6);
}
@foreach (var doc in recentPosts)
{
<article>
<a href="@doc.GetLink()" class="image">
<img src="@(doc.GetString("Image") ?? "/images/pic01.jpg")" alt="@doc.GetTitle()" />
</a>
<h3>@doc.GetTitle()</h3>
<p>@doc.GetString("Description")</p>
<ul class="actions">
<li><a href="@doc.GetLink()" class="button">Read Review</a></li>
</ul>
</article>
}
</div>5. 核心功能
我将向你展示如何在 Statiq 中为静态网站创建核心功能。
5.1 搜索功能
搜索功能是网站必不可少的功能,但我们的网站只是一个静态 HTML 网站,那么该怎么做呢?
我们可以使用 Lunr.js!
Lunr.js 是一个专为客户端使用而设计的轻量级全文搜索库。与需要服务器处理查询的传统搜索引擎(如 Elasticsearch 或 Solr)不同,Lunr 完全在用户的浏览器中运行。
它的工作原理是获取一个 JSON 数据集,在内存中构建一个倒排索引,然后对该索引执行搜索。这使得它成为静态网站(Jamstack)的完美伴侣,因为它不需要任何外部依赖、没有服务器端代码,也不需要 API 密钥。它体积小、速度快,并能处理模糊匹配和权重提升,在静态 HTML 页面上提供了“真正”的搜索体验。
实现步骤:
第一步:生成搜索索引(Statiq 端)
我们需要将所有内容生成到一个 JSON 文件中。在 program.cs 的 ConfigureEngine 中添加以下代码:
.ConfigureEngine(engine =>
{
// Get the default "Content" pipeline
// This is the pipeline where Statiq Web processes all Markdown and Razor files
var contentPipeline = engine.Pipelines["Content"];
// Add a module in the "PostProcess" phase (after all processing is done, but before writing to disk)
contentPipeline.PostProcessModules.Add(
new SetDestination(Config.FromDocument(doc =>
{
// 1. Get the source file name
var sourceName = doc.Source.FileName.ToString();
// 2. Check: If it is our search index file
if (sourceName.Contains("search-index.json"))
{
// [Critical Fix] Use NormalizedPath instead of FilePath
return new NormalizedPath("search-index.json");
}
// 3. Keep other files as they are
return doc.Destination;
}))
);
...
...然后创建一个 Razor 视图(例如 search-index.json.cshtml)。此文件会遍历 Statiq 管道中所有已发布的文档。
它将输出一个包含每个页面的标题、描述、URL 和内容(或关键字)的单一 search-index.json 文件。
这实际上将你的网站内容变成了一个便携式数据库。以下是 search-index.json.cshtml 中的完整代码:
---
Permalink: /search-index.json
---
@using System.Text.Json
@using System.Text.RegularExpressions
@using Statiq.Common
@using System.Net
@{
Layout = null;
// Define the clean function
Func<string, string> CleanText = (input) =>
{
if (string.IsNullOrEmpty(input)) return string.Empty;
// 1. Remove the HTML tags (like <p>, <div>, etc.)
var text = Regex.Replace(input, "<.*?>", " ");
// 2. Decode " HTML tags
text = WebUtility.HtmlDecode(text);
// 3. Remove newlines, tabs, and collapse multiple spaces into one
text = Regex.Replace(text, @"\s+", " ").Trim();
return text;
};
var searchEntries = new List<object>();
foreach (var doc in OutputPages)
{
// 1. Only index HTML files (by extension)
if (doc.Destination.Extension != ".html") continue;
// 2. Must have a title
if (!doc.ContainsKey("Title") || string.IsNullOrEmpty(doc.GetString("Title"))) continue;
// 3. Get the file name
var fileName = doc.Destination.FileName.ToString();
// 4. Exclude the search page, index page, and the search index itself
if (fileName.Equals("search.html", StringComparison.OrdinalIgnoreCase)) continue;
if (fileName.Equals("index.html", StringComparison.OrdinalIgnoreCase)) continue;
if (fileName.Contains("search-index")) continue;
// --- Handle content ---
// Synchronously get the content (since we are in a synchronous context)
string rawContent = doc.GetContentStringAsync().GetAwaiter().GetResult();
// Clean the content
string cleanContent = CleanText(rawContent);
// Get the 5000 first characters of the content (or less if the content is shorter)
if (cleanContent.Length > 5000)
{
cleanContent = cleanContent.Substring(0, 5000);
}
searchEntries.Add(new
{
title = doc.GetString("Title"),
link = doc.GetLink(),
description = doc.GetString("Description") ?? "",
content = cleanContent
});
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
string json = JsonSerializer.Serialize(searchEntries, options);
}
@Html.Raw(json)第二步:加载索引(客户端)
页面加载时,一个简单的 JavaScript 函数会使用 fetch() 获取我们在第一步中生成的 search-index.json 文件。
一旦加载了 JSON,我们就会将其传递给 Lunr。Lunr 会分析文本,去除停用词(如 “the”、“and”),提取词干(将 “running” 转换为 “run”),并构建其内部索引以便快速检索。
我们需要为此创建一个 search.cshtml:
Title: Search Results
Layout: _Layout.cshtml
---
<section>
<header class="main">
<h1>Search Results</h1>
</header>
<div id="search-results-container">
<p>Searching...</p>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.9/lunr.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const params = new URLSearchParams(window.location.search);
const query = params.get("query");
const container = document.getElementById("search-results-container");
if (!query) {
container.innerHTML = "<p>Please enter a search term.</p>";
return;
}
fetch("/search-index.json")
.then(response => response.json())
.then(data => {
const idx = lunr(function () {
this.ref('link');
this.field('title');
this.field('description');
this.field('content');
data.forEach(function (doc) {
this.add(doc);
}, this);
});
const results = idx.search(query);
if (results.length === 0) {
container.innerHTML = "<p>No results found for '" + query + "'.</p>";
} else {
let html = '<div class="posts">';
results.forEach(result => {
const item = data.find(d => d.link === result.ref);
html += `
<article>
<h3>${item.title}</h3>
<p>${item.description || ''}</p>
<ul class="actions">
<li><a href="${item.link}" class="button">Read More</a></li>
</ul>
</article>
`;
});
html += "</div>";
container.innerHTML = html;
}
})
.catch(error => {
console.error("Error:", error);
container.innerHTML = "<p>An error occurred.</p>";
});
});
</script>第三步:执行搜索
好了,我们在这个主题的侧边栏里有一个简单的 HTML 搜索表单,当你提交搜索查询时,Lunr 会查询内存中的索引并返回匹配的文档引用列表(具体来说是 ref ID,它会映射回我们的 URL)。
<section id="search" class="alt">
<form method="get" action="/search">
<input type="text" name="query" id="query" placeholder="Search" />
</form>
</section>局限性与可扩展性:何时需要更换?
虽然 Statiq + Lunr.js 的方法强大、高性价比且保护隐私,但它完全依赖于客户端浏览器。它并非“万能”的解决方案。你需要考虑以下实际限制:
A. “有效载荷”问题(文件大小) 由于用户必须下载整个搜索索引(search-index.json),因此文件大小会随着内容的增加呈线性增长。
- 全文索引:如果你索引每篇文章的整个正文,文件很快就会变得很大。
- 优化:为了缓解这个问题,我建议只索引标题(Title)、标签(Tags)和简短的摘录/描述(Excerpt/Description)。这能在博客不断增长的同时,保持文件轻量。
B. 浏览器性能 Lunr 需要解析 JSON 并在内存中构建倒排索引。对于一个拥有数千个页面的网站,这个过程可能会阻塞主线程,导致在较慢的设备(尤其是手机)上 UI 出现短暂卡顿。
C. 结论:多少页面适用?
- 中小型网站(< 1,000 个页面):这是一个极佳的解决方案。性能影响微乎其微。
- 大型网站(> 2,000 个页面):你可能开始遇到性能瓶颈。在这个规模下,索引文件的大小会成为一种负担。
- 企业级规模:如果你有数以万计的文档,你应该考虑使用服务器端解决方案,如 Algolia、Elasticsearch 或 Azure AI Search。
5.2 标签功能
如果你想创建一个博客网站,我认为你需要一个标签(Tags)功能,你可以为每篇文章添加标签,好的,让我们开始吧!
我们需要在 Program.cs 中添加一个管道来处理标签逻辑:
// We create a new pipeline named "Tags"
engine.Pipelines.Add("Tags", new Pipeline
{
// Depends on the Content pipeline, ensuring articles have been read
Dependencies = { "Content" },
ProcessModules = {
// A. Read all articles processed by the "Content" pipeline
new ReplaceDocuments("Content"),
// B. Filter out articles without Tags to prevent errors
new FilterDocuments(Config.FromDocument(doc => doc.ContainsKey("Tags"))),
// C. Core: Group by the "Tags" field
// This turns 100 articles into (for example) 10 documents, where each document represents a Tag
new GroupDocuments("Tags"),
// D. Load the template we just wrote
new MergeContent(new ReadFiles("_TagLayout.cshtml")),
// E. Render Razor
new RenderRazor()
.WithModel(Config.FromDocument((doc, ctx) => doc)),
// F. Set output path: /tag/tag-name.html
new SetDestination(Config.FromDocument(doc =>
{
var tagName = doc.GetString(Keys.GroupKey);
var slug = tagName.ToLower().Replace(" ", "-");
return new NormalizedPath($"tag/{slug}.html");
})),
// [Critical Fix] G. Write files!
// This step is mandatory, otherwise the files only exist in memory
new WriteFiles()
}
});创建 _TagLayout.cshtml:
@{
Layout = "_Layout.cshtml";
// Get the current tags name
var tagName = Document.GetString(Keys.GroupKey);
}
<section>
<header class="main">
<h1>Tag: <em>@tagName</em></h1>
</header>
<div class="posts">
@foreach (var post in Document.GetChildren())
{
<article>
@if (post.ContainsKey("Image"))
{
<a href="@post.GetLink()" class="image">
<img src="@post.GetString("Image")" alt="" />
</a>
}
<h3><a href="@post.GetLink()">@post.GetString("Title")</a></h3>
<p>@post.GetString("Description")</p>
<ul class="actions">
<li><a href="@post.GetLink()" class="button">Read More</a></li>
</ul>
</article>
}
</div>
</section>结果如下。

此外我们还可以做得更多,为了提高 SEO,我们可以根据标签自动为文章生成内部链接。
在项目根目录下的 TagAutoLinkModule.cs 中创建一个 ParallelModule:
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Statiq.Common;
public class TagAutoLinkModule : ParallelModule
{
protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
{
// 1. Retrieve all tags and their corresponding URLs from the entire context
// Assuming your tag page path format is /tags/tag-name
var allTags = context.Outputs
.SelectMany(doc => doc.GetList<string>("Tags") ?? Enumerable.Empty<string>()) // Directly use the string "Tags"
.Distinct()
.OrderByDescending(t => t.Length)
.ToDictionary(
tag => tag,
tag => $"/tag/{tag.ToLower().Replace(" ", "-")}"
);
string content = await input.GetContentStringAsync();
// 2. Iterate through all tags to process the current article
foreach (var tag in allTags)
{
// Prevent in-article tags from linking to themselves (optional)
// string pattern = $@"(?<!<[^>]*){tag}(?![^<]*</a>)";
// Complex Regex: Match tags, but exclude cases where they are already inside <a> tags or HTML attributes
string pattern = $@"\b({Regex.Escape(tag.Key)})\b(?![^<]*>)(?![^<]*</a>)";
content = Regex.Replace(content, pattern, $"<a href=\"{tag.Value}\" class=\"internal-tag-link\">$1</a>", RegexOptions.IgnoreCase);
}
return input.Clone(context.GetContentProvider(content, "text/html")).Yield();
}
}它会找到匹配标签的关键字并添加超链接。在管道中处理该模块:
ModifyPipeline(nameof(Statiq.Web.Pipelines.Content), pipeline =>
{
// Add custom module to PostModules
// This ensures it runs after all standard processing (like Markdown rendering) is complete
pipeline.ProcessModules.Add(new TagAutoLinkModule());
})你可以在下面看到结果。

自动添加超链接以匹配标签。
5.3 分页功能
对于静态网站来说,分页也是一个问题,但 Statiq 可以帮你处理好它。
例如,我们有一个列表页面模板 _ListLayout.cshtml,用于显示一个分类下的所有文章。
我们需要从 input 文件夹获取文章:
@{
// 1. Define the variable IEnumerable<IDocument>
IEnumerable<IDocument> posts;
// 2. Get the children docs
var childDocs = Document.GetChildren();
if (childDocs.Any())
{
posts = childDocs;
}
else
{
posts = OutputPages.GetChildrenOf(Document)
.Where(x => x.Source.Extension == ".md" && x.Id != Document.Id)
.OrderByDescending(x => x.Get<DateTime>("Date", DateTime.MinValue));
}
}
@foreach (var post in posts)
{
<article>
@if (post.ContainsKey("Image"))
{
<a href="@post.GetLink()" class="image">
<img src="@post.GetString("Image")" alt="" />
</a>
}
<h3><a href="@post.GetLink()">@post.GetString("Title")</a></h3>
<p>@post.GetString("Description")</p>
<ul class="actions">
<li><a href="@post.GetLink()" class="button">Read More</a></li>
</ul>
</article>
}并处理分页逻辑:
@{
// Get the previouse and next page documents
var prevPage = Document.GetDocument(Keys.Previous);
var nextPage = Document.GetDocument(Keys.Next);
// Get the total pages and current page number
var totalPages = Document.GetInt(Keys.TotalPages);
var currentPage = Document.GetInt(Keys.Index);
}
@if (totalPages > 1)
{
<div class="pagination" style="margin-top: 3rem; text-align: center;">
@if (prevPage != null)
{
<a href="@prevPage.GetLink()" class="button">Prev</a>
}
else
{
<span class="button disabled">Prev</span>
}
<span class="extra" style="margin: 0 1rem;">
Page @currentPage of @totalPages
</span>
@if (nextPage != null)
{
<a href="@nextPage.GetLink()" class="button">Next</a>
}
else
{
<span class="button disabled">Next</span>
}
</div>
}5.4 归档
最后一个功能是显示归档。这也是博客网站的常见功能。
实际上,我们只需要对文章进行分组和排序,就可以获取归档数据了。
var allPosts = OutputPages
.Where(x => x.Source != null
&& x.Source.ToString().Contains("posts")
&& x.Source.Extension == ".md"
&& x.ContainsKey("Date"));
var postsByYear = allPosts
.OrderByDescending(x => x.Get<DateTime>("Date"))
.GroupBy(x => x.Get<DateTime>("Date").Year);创建一个 archives.cshtml 模板来显示数据:
foreach (var yearGroup in postsByYear)
{
<h2 id="year-@yearGroup.Key" style="border-bottom: 2px solid #f56a6a; padding-bottom: 10px; margin-top: 2em;">
@yearGroup.Key
</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 120px;">Date</th>
<th>Title</th>
<th>Category / Tags</th>
</tr>
</thead>
<tbody>
@foreach (var post in yearGroup)
{
var postDate = post.Get<DateTime>("Date");
<tr>
<td style="white-space: nowrap; width: 120px;">
@postDate.ToString("MMM dd")
</td>
<td>
<a href="@post.GetLink()" style="font-weight: bold;">
@post.GetString("Title")
</a>
</td>
<td>
@if (post.ContainsKey("Tags"))
{
foreach (var tag in post.GetList<string>("Tags").Take(3))
{
<span class="button small"
style="font-size: 0.6em; height: 2em; line-height: 2em; padding: 0 10px; margin-right: 2px;">
@tag
</span>
}
}
</td>
</tr>
}
</tbody>
</table>
</div>
}6. 创建内容
创建好主题和功能后,我们就可以开始创建内容了。Statiq 会从 input 文件夹中读取内容,所以我们必须把内容放在那里。并且我们可以使用 Markdown 来编写我们的内容。
例如,在我的网站中,我将所有文章放入 input/posts 文件夹,将图片放入 input/images 文件夹,并创建如下一篇文章:
---
Title: "The 10 Best Dinnerware Sets of 2026: Trends, Reviews & Buying Guide"
Description: "We tested the top dinnerware sets of 2026. From durable Corelle to luxurious Wedgwood bone china, here are the best plates for every budget and style."
Date: 2026-02-08
Layout: "_PostLayout"
Image: "/images/hero-best-dinnerware-2026.webp"
Tags: [Buying Guide, 2026 Trends, Bone China, Stoneware, Luxury]
---
Other content below ......你需要在文件顶部以 Markdown 格式设置元数据,然后主题就会读取它们。
7. 部署
每次你更改内容或主题时,Statiq 都会生成所有的 HTML 文件,因此,如果你只是将全部或更新的文件上传到 FTP,那也会是个问题。
如果你的主机提供 SSH 和 Linux 操作系统,那么下面的部署方式将非常适合你!
你可以使用 rsync 仅将更改的文件上传到你的服务器,而且你不需要逐个选择或检查哪些文件应该被上传。
如果你使用的是 Mac OS,那么 rsync 是一个内置命令;如果你使用的是 Windows,那么你需要安装 Git Bash 或 WSL。
要检查你的主机是否支持 rsync,你可以使用以下命令:
ssh username@your-server.com "rsync --version"好了,你可以使用下面的脚本将你的 HTML 网站部署到服务器上:
#!/bin/bash
# 1. the ssh key
KEY_PATH="~/.ssh/ssh.key"
# 2. VPS Information
USER="root"
IP="VPS IP"
REMOTE_DIR="/opt/www/sites/yourdomain.com/"
echo "Generating static website..."
dotnet run
echo "Syncing output to server..."
rsync -avz --delete --rsync-path="sudo rsync" -e "ssh -i $KEY_PATH" output/ ${USER}@${IP}:${REMOTE_DIR}
echo "Deployment complete!"就是这么简单!😁
完整的项目和主题在此
你可以在下方找到我的完整项目:
8. 结论
对我来说,发现 Statiq 是一个改变游戏规则的时刻。它证明了我不需要为了构建一个快速、安全的网站而去学习一个新的 JavaScript 框架。Statiq 强大的生成管道和 Lunr.js 轻量级的简单性相结合,为我的需求创造了完美的架构。
无论你是在构建作品集、公司博客,还是文档网站,这个技术栈都提供了令人难以置信的速度和灵活性。我鼓励你克隆代码库,调整管道,并构建属于你自己的东西。.NET 静态网站社区正在不断壮大,现在正是加入的最佳时机。
版权声明:
作者:winson
链接:https://www.coderblog.cc/2026/03/a-static-site-generator-that-speaks-c-getting-started-with-statiq/
来源:代码部落中文站
文章版权归作者所有,未经允许请勿转载。









