WitDocs is a static site generator for .NET developers. It's built on Blazor WebAssembly, uses markdown for content, and produces fast, SEO-optimized static sites that deploy anywhere.

If you've ever spent a weekend fighting webpack configs just to put up a docs site for your .NET library — or wished you could stay in the C# ecosystem for your blog — this is for you.


The Problem

You've built something in .NET. A library, a tool, a product. Now you need a website: documentation, blog posts, a landing page. What are your options?

Jekyll — Ruby. You need to install Ruby, deal with Bundler, learn Liquid templates. It works, but you're now maintaining a Ruby toolchain for a .NET project.

Hugo — Go. Fast, powerful. Also a completely different ecosystem with its own templating language. Your IDE doesn't help you here.

Docusaurus, Gatsby, Next.js — JavaScript. Now you need Node.js, npm, package.json, and 400MB of node_modules. For a docs site.

GitHub Wiki / README.md — Free, zero setup. Also zero customization, no search, no theming, no blog, no SEO.

Every option pulls you out of the .NET world. You end up context-switching between your actual project and a completely unrelated tech stack just to publish some documentation.


What is WitDocs?

WitDocs is three NuGet packages that give you a complete static site:

Package What It Does
OutWit.Web.Framework Blazor components, layout, theming, content services
OutWit.Web.Generator CLI tool: sitemap, search index, RSS, OG images, static HTML
OutWit.Web.Templates dotnet new template to scaffold a site in seconds

You write markdown. The framework renders it. The generator creates everything search engines and social media need. You deploy static files to any CDN.

The entire toolchain is .NET. Your IDE understands it. Your CI/CD already has the SDK. Your muscle memory works.


Five Minutes to a Live Site

# Install the template
dotnet new install OutWit.Web.Templates

# Scaffold a new site
dotnet new outwit-web -n MySite \
  --site-name "My Project" \
  --base-url "https://myproject.dev" \
  --accent-color "#7C3AED"

# Run it
cd MySite
dotnet run

That's it. Open your browser and you have a working site with navigation, theming, and a blog section ready for content.

Adding Content

Drop a markdown file into wwwroot/content/blog/:

---
title: 'First Post'
description: 'Hello world from my new site.'
publishDate: 2026-02-06
tags: [announcement]
---

This is my first blog post. It supports **full markdown** 
with code blocks, tables, images, and more.

Refresh. It's there. No build step during development — Blazor loads content directly.


What You Get Out of the Box

Content Types

WitDocs isn't just for docs. It handles multiple content types, each with its own URL structure and list page:

Content Type Folder URL Pattern
Blog posts content/blog/ /blog/{slug}
Projects content/projects/ /project/{slug}
Articles content/articles/ /article/{slug}
Documentation content/docs/ /docs/{slug}
Custom sections Configurable Configurable

Each type gets automatic list pages, pagination, tag filtering, and reading time estimates.

SEO That Actually Works

Every page gets proper metadata without you thinking about it:

Feature What It Does
Open Graph tags Facebook, LinkedIn, Slack previews look good
Twitter Cards Proper image + description
JSON-LD structured data Search engines understand your content
Sitemap.xml Auto-generated with correct lastmod dates
robots.txt Configured for your hosting provider
Canonical URLs No duplicate content issues
Static HTML pre-rendering Crawlers see real content, not a blank Blazor shell

Auto-Generated OG Images

Social media preview images are generated automatically for every page using Playwright. They pick up your theme colors, show the title, description, and content type. No design work needed.

A pre-built search index ships with your site. Users can search all content instantly, no server required. The index is generated at build time and loaded on demand.

Light/Dark Themes

Theme support is built into the CSS framework. Define your colors in theme.css and the entire site respects them:

:root {
    --color-background: #f8f9fc;
    --color-accent: #7C3AED;
    /* ... */
}

[data-theme="dark"] {
    --color-background: #11131e;
    --color-accent: #a78bfa;
}

A toggle in the header lets users switch. Their preference is remembered.

RSS Feed

Blog posts automatically get an RSS feed at /feed.xml. Subscribers see new posts without checking the site.


Project Structure

A WitDocs site is a standard Blazor WebAssembly project:

MySite/
    Pages/                   # Thin Razor wrappers
        Index.razor
        Blog.razor
        BlogPost.razor
        Contact.razor
        Search.razor
    wwwroot/
        content/             # Markdown content
            blog/
            projects/
            docs/
        css/
            theme.css        # Your color scheme
            site.css         # Optional custom styles
        images/
            logo-light.svg
            logo-dark.svg
        site.config.json     # Navigation, footer, SEO
    Program.cs
    MySite.csproj

Pages are intentionally thin — they delegate to framework components:

@page "/blog/{Slug}"

<BlogPostPage Slug="@Slug" />

@code {
    [Parameter] public string Slug { get; set; } = "";
}

Five lines. The framework handles loading the markdown, parsing frontmatter, rendering HTML, injecting SEO tags, and building the navigation.


Configuration

Everything is in site.config.json:

{
  "siteName": "My Project",
  "baseUrl": "https://myproject.dev",
  "logoLight": "/images/logo-light.svg",
  "logoDark": "/images/logo-dark.svg",
  "defaultTheme": "dark",
  "navigation": [
    { "title": "Home", "href": "/" },
    { "title": "Blog", "href": "/blog" },
    { "title": "Docs", "href": "/docs" },
    { "title": "Contact", "href": "/contact" }
  ],
  "footer": {
    "copyright": "My Company",
    "socialLinks": [
      { "platform": "github", "url": "https://github.com/my-org" },
      { "platform": "twitter", "url": "https://twitter.com/my-org" }
    ]
  }
}

No YAML. No TOML. JSON that your IDE validates and autocompletes.


The Build Pipeline

During development, content loads directly — fast iteration, no generators.

On dotnet publish -c Release, the generator runs automatically and creates:

Generated File Purpose
content/index.json Content manifest
navigation-index.json Pre-built menu data for instant nav rendering
content-metadata.json Lightweight metadata for fast list pages
sitemap.xml SEO sitemap
robots.txt Crawler rules
search-index.json Client-side search index
feed.xml RSS feed
*/index.html Static HTML for every page (SEO + fast first paint)
Hosting config _headers, _routes.json, etc. per provider

Performance Optimizations

The generator pre-computes everything it can so the client does minimal work:

  • Navigation index — the header menu renders instantly without parsing markdown files
  • Content metadata index — list pages (blog, home) load without parsing every post
  • Static HTML — search engines and social media crawlers see real content immediately
  • OG images — generated once, cached, only regenerated when content changes

Deployment

WitDocs produces static files. Deploy them anywhere.

Cloudflare Pages

- name: Build
  run: dotnet publish -c Release -o publish

- name: Deploy
  uses: cloudflare/pages-action@v1
  with:
    directory: publish/wwwroot

Netlify, Vercel, GitHub Pages

Same idea — publish, upload the wwwroot folder. The generator creates hosting-specific config files (_redirects, vercel.json, .nojekyll) automatically based on your <OutWitHostingProvider> MSBuild property.


Custom Sections

Need a "Solutions" page? A "Use Cases" section? Add it in config:

{
  "contentSections": [
    { "folder": "solutions", "route": "solutions", "menuTitle": "Solutions" }
  ]
}

Create content/solutions/, add markdown files. The framework handles routing, list pages, and the generator includes them in search and sitemap.


Component Extensibility

WitDocs isn't a black box. Every component is a standard Blazor component with [Parameter] properties and RenderFragment slots. You customize through composition, not configuration files.

Parameters Control Behavior

Every page component exposes parameters that let you adjust its behavior without touching the framework source:

<HomePage SeoTitle="My Product — Build Better Software"
          SeoDescription="Tools for .NET developers."
          ProjectsSectionTitle="Our Tools" />

The same HomePage component renders differently on ratner.io and witdatabase.io — same framework, different parameters.

RenderFragment Slots for Custom Content

Parameters handle text and flags. For rich custom content, components expose RenderFragment slots — typed placeholders where you inject your own markup.

The HomePage has a HeroContent slot. Here's how ratner.io uses it for a personal portfolio:

@page "/"

<HomePage SeoTitle="OutWit - Embrace Efficiency">
    <HeroContent>
        <HeroFlat Title="OUTWIT COMPLEXITY<br/>EMBRACE EFFICIENCY"
                  AuthorName="Dmitry Ratner"
                  AuthorTitle="Software Development Expert" />
    </HeroContent>
</HomePage>

And here's how WitEngine's landing page skips HomePage entirely and composes its own layout from framework components — a product hero with logo, tagline, and a GitHub button with a custom SVG icon:

@page "/"

<HeroRect LogoDarkUrl="/images/logo-dark.svg"
          LogoLightUrl="/images/logo-light.svg"
          Title="WitEngine"
          Tagline="Distributed computing made simple."
          PrimaryButtonText="Get Started"
          PrimaryButtonUrl="/learn/what-is-witengine"
          SecondaryButtonText="GitHub"
          SecondaryButtonUrl="https://github.com/dmitrat/WitEngine.Public">
    <SecondaryButtonIcon>
        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 
             viewBox="0 0 24 24" fill="currentColor">
            <path d="M12 0c-6.626 0-12 5.373-12 12 ..."/>
        </svg>
    </SecondaryButtonIcon>
</HeroRect>

Same framework. Same HeroRect component. But the secondary button now has a custom SVG icon — injected via the SecondaryButtonIcon render fragment, not through a string parameter or a config file.

Mix Built-In and Custom Components

The framework provides hero sections (HeroFlat, HeroRect), feature grids, content cards, loading states with skeleton placeholders, page headers, and more. You can use them as-is, customize them with parameters, or skip them entirely and write your own.

FeatureGrid, for example, works in two modes. Give it no children, and it loads features from markdown files in content/features/:

<FeatureGrid Title="Why WitDatabase?" />

Or provide your own cards:

<FeatureGrid Title="Features">
    <FeatureCard Icon="🔒" Title="Encryption" 
                 Description="AES-256-GCM and ChaCha20." />
    <FeatureCard Title="Cross-Platform">
        <IconContent>
            <svg><!-- custom SVG --></svg>
        </IconContent>
        <DescriptionContent>
            Works on <strong>all</strong> .NET platforms.
        </DescriptionContent>
    </FeatureCard>
</FeatureGrid>

Even within a single card, you choose the level of customization: simple emoji icon or custom SVG via IconContent, plain text description or rich HTML via DescriptionContent.

Thin Pages, Rich Components

Your Razor pages stay minimal — they only compose and configure:

@page "/internals/{Slug}"

<ArticlePage Slug="@Slug" ContentFolder="internals" />

@code {
    [Parameter] public string Slug { get; set; } = "";
}

The ArticlePage component handles everything: loading markdown, parsing frontmatter, rendering HTML, building the table of contents sidebar, highlighting the active heading on scroll, and navigating to prev/next pages. Your page just tells it which content folder to use.

This pattern means adding a new content section is a two-step process: create a Razor page (3 lines), add a content folder. The framework handles the rest.

Loading States and Skeletons

Every content component supports loading states with customizable skeleton placeholders:

<LoadingState IsLoading="@Loading" HasContent="@(Posts.Any())">
    <SkeletonContent>
        @for (int i = 0; i < 3; i++)
        {
            <ContentCardSkeleton ShowTags="true" />
        }
    </SkeletonContent>
    <ChildContent>
        @foreach (var post in Posts)
        {
            <BlogCard Post="@post" />
        }
    </ChildContent>
</LoadingState>

While content loads, users see animated skeleton shapes instead of a blank page. This is built into the framework — you don't write CSS shimmer animations yourself.

The Extensibility Model

The pattern is consistent across every component:

Extension Point Mechanism Example
Text, flags, numbers [Parameter] properties Title, SeoTitle, ShowToc, MaxItems
Rich custom content RenderFragment slots HeroContent, ChildContent, IconContent
Contextual data CascadingValue Parent page passes anchor links to child heroes
Content-driven Markdown files Features, docs, blog posts loaded from content/ folders
Full override Write your own component Replace any component entirely — it's standard Blazor

There's no plugin API to learn, no hook system to understand. If you know Blazor, you know how to extend WitDocs.


Embedded Components in Markdown

Markdown is great for text, but sometimes you need more — a YouTube video, an SVG diagram, an image that floats with text wrapping. Most static site generators solve this by switching to MDX (Markdown + JSX) or shortcodes with limited options.

WitDocs takes a different approach: you embed Blazor components directly in markdown using a double-bracket syntax, and any Blazor component can be registered as embeddable.

Built-In Components

Three components ship with the framework:

<!-- Embed a YouTube video -->
[[YouTube videoId="dQw4w9WgXcQ"]]

<!-- Inline SVG diagram with relative path -->
[[Svg Src="./architecture-diagram.svg" Alt="System architecture"]]

<!-- Image floating to the right with text wrapping around it -->
[[FloatingImage src="./photo.jpg" position="right" width="300"]]

This text wraps around the image on the left side.
You can write as many paragraphs as needed.

[[/FloatingImage]]

Notice the two syntaxes. Self-closing components like YouTube render at that point in the content. Block components like FloatingImage wrap content — the markdown between the opening and closing tags becomes the component's InnerContent and gets rendered alongside it.

Parameters use familiar HTML-like syntax: param="value", param='value', or param=value for simple values. They're case-insensitive.

How It Works

When WitDocs loads a markdown file, the ContentParser scans for embedded component blocks, extracts them, and replaces each with an HTML comment placeholder. The markdown is then rendered to HTML normally. At render time, ContentWithComponents walks the HTML, and wherever it finds a placeholder, it uses Blazor's DynamicComponent to render the real component with the extracted parameters.

The result: your markdown stays clean, and the embedded components are full Blazor components with access to dependency injection, async loading, and the entire .NET runtime.

Registering Custom Components

This is where it gets interesting. The ComponentRegistry maps type names to Blazor component types:

public class ComponentRegistry
{
    public ComponentRegistry()
    {
        // Built-in
        Register("YouTube", typeof(YouTube));
        Register("FloatingImage", typeof(FloatingImage));
        Register("Svg", typeof(Svg));
    }

    public void Register(string typeName, Type componentType) { ... }
}

To add your own component, write a standard Blazor component and register it. Say you want to embed interactive code playgrounds in your documentation:

@* Components/Content/CodePlayground.razor *@
<div class="code-playground">
    <pre><code>@Code</code></pre>
    <button @onclick="Run">Run</button>
    <div class="output">@Output</div>
</div>

@code {
    [Parameter] public string Code { get; set; } = "";
    [Parameter] public string Language { get; set; } = "csharp";
    [Parameter] public string BasePath { get; set; } = "";
    
    private string Output = "";
    private void Run() { /* ... */ }
}

Register it in your Program.cs:

var registry = builder.Services
    .BuildServiceProvider()
    .GetRequiredService<ComponentRegistry>();

registry.Register("CodePlayground", typeof(CodePlayground));

Now use it in any markdown file:

## Try It Yourself

[[CodePlayground Language="csharp" Code="Console.WriteLine(\"Hello!\");"]]

The framework handles everything else: parsing the double-bracket syntax, extracting parameters, resolving them to your component's [Parameter] properties, and rendering the component inline with the surrounding HTML.

Block Components for Wrapping Content

Block components receive the markdown between their tags as InnerContent. This is how FloatingImage creates text-wrap layouts — it renders the image, then the inner markdown flows around it:

[[FloatingImage src="./team-photo.jpg" position="left" width="350"]]

## Our Team

We're a small group of engineers based in Tel Aviv,
passionate about building developer tools that
respect your time and your existing workflows.

[[/FloatingImage]]

You can use this pattern for callout boxes, expandable sections, side-by-side comparisons — any layout where content needs to be wrapped or transformed by a component.

Relative Path Resolution

Embedded components automatically resolve relative paths. If your blog post lives at content/blog/2026-02-06-my-post/index.md, then an Svg component with Src="./diagram.svg" resolves to content/blog/2026-02-06-my-post/diagram.svg. Keep your assets next to your content — no global image folders, no broken links when you reorganize.


Why Blazor WebAssembly?

A fair question. Why not a traditional SSG that outputs plain HTML?

Three reasons:

Client-side interactivity without JavaScript. Search, theme switching, dynamic navigation — all in C#. No bundle.js to maintain.

Component reuse. The framework components are real Blazor components. If you're building a Blazor app elsewhere, you already know the model. If you need to add a custom interactive widget to your docs site, you write a .razor file, not a React component.

Static HTML too. The generator pre-renders every page as static HTML. Crawlers see real content. First paint is fast. Blazor hydrates after loading and takes over for smooth navigation. You get the best of both worlds.


Comparison

Feature WitDocs Jekyll Hugo Docusaurus
Language C# / .NET Ruby Go JavaScript
Content Markdown Markdown Markdown MDX
Embedded components Blazor components in markdown Liquid includes Shortcodes React in MDX
Search Built-in Plugin Plugin Built-in
Themes CSS variables Gem-based Module-based CSS modules
SEO Auto-generated Manual Manual Auto-generated
OG Images Auto-generated Manual Manual Manual
Deploy Any static host Any static host Any static host Any static host
Setup dotnet new gem install brew install npx create

WitDocs doesn't try to be the most powerful SSG. It tries to be the one that makes sense when you're already in the .NET ecosystem.


Getting Started

# Install the template
dotnet new install OutWit.Web.Templates

# Create your site
dotnet new outwit-web -n MyDocs --site-name "My Project Docs"

# Start writing
cd MyDocs
dotnet run

The source code is on GitHub. The framework, generator, and templates are all on NuGet: